Skip to content

Event System

The Hytale event system provides a powerful way to react to game occurrences and enable inter-plugin communication.

EventBus (Global)
├── SyncEventBusRegistry - Synchronous events (IEvent)
└── AsyncEventBusRegistry - Asynchronous events (IAsyncEvent)
EventRegistry (Per-Plugin)
└── Wraps EventBus with lifecycle management

Execute handlers immediately in priority order:

public interface IEvent<KeyType> extends IBaseEvent<KeyType> {
}

Execute handlers with CompletableFuture chaining:

public interface IAsyncEvent<KeyType> extends IBaseEvent<KeyType> {
}

Events that can be cancelled to prevent default behavior:

public interface ICancellable {
boolean isCancelled();
void setCancelled(boolean var1);
}

Events are dispatched in priority order:

PriorityValueDescription
FIRST-21844Execute first
EARLY-10922Execute early
NORMAL0Default priority
LATE10922Execute late
LAST21844Execute last
@Override
protected void setup() {
getEventRegistry().register(BootEvent.class, this::onBoot);
}
private void onBoot(BootEvent event) {
getLogger().info("Server booted!");
}
getEventRegistry().register(
EventPriority.EARLY,
PlayerJoinEvent.class,
event -> {
// Handle early
}
);
// Or with custom priority value
getEventRegistry().register(
(short) -5000,
PlayerJoinEvent.class,
this::onPlayerJoin
);

Listen to events for specific contexts:

// Listen to events for specific world
getEventRegistry().register(
WorldEvent.class,
"world_name", // Key
event -> {
// Only fires for events in "world_name"
}
);

Listen to all instances regardless of key:

getEventRegistry().registerGlobal(
EntitySpawnEvent.class,
event -> {
// Handles all entity spawns in all worlds
}
);
public class MyEvent implements IEvent<Void> {
private final String data;
public MyEvent(String data) {
this.data = data;
}
public String getData() {
return data;
}
}
public class WorldSpecificEvent implements IEvent<String> {
private final String worldName;
private final int value;
public WorldSpecificEvent(String worldName, int value) {
this.worldName = worldName;
this.value = value;
}
public String getWorldName() {
return worldName;
}
public int getValue() {
return value;
}
}
public class CancellableEvent implements IEvent<Void>, ICancellable {
private boolean cancelled = false;
private final String action;
public CancellableEvent(String action) {
this.action = action;
}
@Override
public boolean isCancelled() {
return cancelled;
}
@Override
public void setCancelled(boolean cancelled) {
this.cancelled = cancelled;
}
public String getAction() {
return action;
}
}
public class MyAsyncEvent implements IAsyncEvent<Void>, ICancellable {
private boolean cancelled = false;
private String result;
@Override
public boolean isCancelled() { return cancelled; }
@Override
public void setCancelled(boolean cancelled) { this.cancelled = cancelled; }
public String getResult() { return result; }
public void setResult(String result) { this.result = result; }
}
IEventDispatcher<MyEvent, MyEvent> dispatcher =
HytaleServer.get().getEventBus().dispatchFor(MyEvent.class);
if (dispatcher.hasListener()) {
MyEvent event = new MyEvent("data");
dispatcher.dispatch(event);
}
IEventDispatcher<WorldEvent, WorldEvent> dispatcher =
HytaleServer.get().getEventBus().dispatchFor(
WorldEvent.class,
worldName // Key
);
dispatcher.dispatch(new WorldEvent(worldName, value));
HytaleServer.get().getEventBus()
.dispatchForAsync(PlayerChatEvent.class)
.dispatch(new PlayerChatEvent(sender, targets, message))
.whenComplete((event, error) -> {
if (error != null || event.isCancelled()) {
return;
}
sendMessage(event.getTargets(), event.getMessage());
});
EventKey TypeDescription
BootEventVoidServer fully booted
ShutdownEventVoidServer shutting down (has priority constants: DISCONNECT_PLAYERS, UNBIND_LISTENERS, SHUTDOWN_WORLDS)
PluginSetupEventVoidPlugin setup completed
PrepareUniverseEventVoidUniverse preparation
EventKey TypeCancellableDescription
AddWorldEventStringYesWorld added to universe
RemoveWorldEventStringYesWorld removed (cannot cancel if RemovalReason.EXCEPTIONAL)
AllWorldsLoadedEventVoidNoAll worlds finished loading
StartWorldEventStringNoWorld started
EventKey TypeAsyncCancellableDescription
PlayerConnectEventVoidNoNoPlayer connecting, can set initial world
PlayerDisconnectEventVoidNoNoPlayer disconnected, provides DisconnectReason
PlayerChatEventStringYesYesPlayer chat message with customizable formatter
PlayerCraftEventStringNoNoPlayer crafting (deprecated)
PlayerSetupConnectEventVoidNoYesPlayer setup phase connection
PlayerSetupDisconnectEventVoidNoNoPlayer setup phase disconnection
AddPlayerToWorldEventStringNoNoPlayer added to a world
DrainPlayerFromWorldEventStringNoNoPlayer removed from a world
PlayerReadyEventStringNoNoPlayer ready to play
EventKey TypeDescription
EntityRemoveEventVoidEntity removed from world
LivingEntityInventoryChangeEventStringLiving entity inventory changed
LivingEntityUseBlockEventStringLiving entity uses a block
EventCancellableDescription
PlaceBlockEventYesBlock placed, provides ItemStack, Vector3i, and RotationTuple
BreakBlockEventYesBlock broken, provides ItemStack, Vector3i, and BlockType
DamageBlockEventYesBlock damaged
UseBlockEventYesBlock used/interacted with
EventCancellableDescription
CraftRecipeEventYesRecipe crafted
DropItemEventYesItem dropped
InteractivelyPickupItemEventYesItem picked up interactively
SwitchActiveSlotEventYesActive slot switched
ChangeGameModeEventYesGame mode changed
DiscoverZoneEventYesZone discovered
EventKey TypeDescription
LoadedAssetsEventVoidAssets loaded, provides asset map and query
RemovedAssetsEventVoidAssets removed, indicates if replaced
GenerateAssetsEventVoidAsset generation, implements IProcessedEvent
EventKey TypeDescription
GroupPermissionChangeEventVoidGroup permission changed
PlayerPermissionChangeEventVoidPlayer permission changed
PlayerGroupEventVoidPlayer group changed

Event registrations can be removed when no longer needed:

EventRegistration<Void, BootEvent> registration =
getEventRegistry().register(BootEvent.class, this::onBoot);
// Later, unregister when done
registration.close();

The EventRegistry provided by plugins automatically handles cleanup when the plugin is unloaded.

For async events, use the registerAsync method with a Function that transforms the CompletableFuture:

getEventRegistry().registerAsync(
PlayerChatEvent.class,
future -> future.thenApply(event -> {
// Modify the event asynchronously
event.setContent(event.getContent().toUpperCase());
return event;
})
);

Register handlers that only fire when no other handler processed the event:

getEventRegistry().registerUnhandled(
CustomEvent.class,
event -> {
// This only fires if no keyed handlers matched
getLogger().info("Unhandled event: " + event);
}
);
  1. Use appropriate priority - Don’t always use FIRST/LAST
  2. Check hasListener() - Avoid creating events when no one listens
  3. Handle async properly - Don’t block in async handlers
  4. Respect cancellation - Check isCancelled() before actions
  5. Use keyed events - For scoped/efficient event handling
  6. Clean exception handling - Exceptions are logged but don’t stop other handlers
  7. Use registerGlobal for cross-key listeners - When you need to handle all instances of a keyed event
  8. Prefer async events for I/O operations - Avoid blocking the main thread