Event System
The Hytale event system provides a powerful way to react to game occurrences and enable inter-plugin communication.
Architecture
Section titled “Architecture”EventBus (Global)├── SyncEventBusRegistry - Synchronous events (IEvent)└── AsyncEventBusRegistry - Asynchronous events (IAsyncEvent)
EventRegistry (Per-Plugin)└── Wraps EventBus with lifecycle managementEvent Types
Section titled “Event Types”Synchronous Events (IEvent)
Section titled “Synchronous Events (IEvent)”Execute handlers immediately in priority order:
public interface IEvent<KeyType> extends IBaseEvent<KeyType> {}Asynchronous Events (IAsyncEvent)
Section titled “Asynchronous Events (IAsyncEvent)”Execute handlers with CompletableFuture chaining:
public interface IAsyncEvent<KeyType> extends IBaseEvent<KeyType> {}Cancellable Events
Section titled “Cancellable Events”Events that can be cancelled to prevent default behavior:
public interface ICancellable { boolean isCancelled(); void setCancelled(boolean var1);}Event Priorities
Section titled “Event Priorities”Events are dispatched in priority order:
| Priority | Value | Description |
|---|---|---|
FIRST | -21844 | Execute first |
EARLY | -10922 | Execute early |
NORMAL | 0 | Default priority |
LATE | 10922 | Execute late |
LAST | 21844 | Execute last |
Subscribing to Events
Section titled “Subscribing to Events”Basic Registration
Section titled “Basic Registration”@Overrideprotected void setup() { getEventRegistry().register(BootEvent.class, this::onBoot);}
private void onBoot(BootEvent event) { getLogger().info("Server booted!");}With Priority
Section titled “With Priority”getEventRegistry().register( EventPriority.EARLY, PlayerJoinEvent.class, event -> { // Handle early });
// Or with custom priority valuegetEventRegistry().register( (short) -5000, PlayerJoinEvent.class, this::onPlayerJoin);Keyed Events
Section titled “Keyed Events”Listen to events for specific contexts:
// Listen to events for specific worldgetEventRegistry().register( WorldEvent.class, "world_name", // Key event -> { // Only fires for events in "world_name" });Global Registration
Section titled “Global Registration”Listen to all instances regardless of key:
getEventRegistry().registerGlobal( EntitySpawnEvent.class, event -> { // Handles all entity spawns in all worlds });Creating Custom Events
Section titled “Creating Custom Events”Simple Synchronous Event
Section titled “Simple Synchronous Event”public class MyEvent implements IEvent<Void> { private final String data;
public MyEvent(String data) { this.data = data; }
public String getData() { return data; }}Keyed Event
Section titled “Keyed Event”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; }}Cancellable Event
Section titled “Cancellable Event”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; }}Async Event
Section titled “Async Event”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; }}Dispatching Events
Section titled “Dispatching Events”Basic Dispatch
Section titled “Basic Dispatch”IEventDispatcher<MyEvent, MyEvent> dispatcher = HytaleServer.get().getEventBus().dispatchFor(MyEvent.class);
if (dispatcher.hasListener()) { MyEvent event = new MyEvent("data"); dispatcher.dispatch(event);}With Key
Section titled “With Key”IEventDispatcher<WorldEvent, WorldEvent> dispatcher = HytaleServer.get().getEventBus().dispatchFor( WorldEvent.class, worldName // Key );
dispatcher.dispatch(new WorldEvent(worldName, value));Async Dispatch
Section titled “Async Dispatch”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()); });Built-in Events
Section titled “Built-in Events”Server Lifecycle
Section titled “Server Lifecycle”| Event | Key Type | Description |
|---|---|---|
BootEvent | Void | Server fully booted |
ShutdownEvent | Void | Server shutting down (has priority constants: DISCONNECT_PLAYERS, UNBIND_LISTENERS, SHUTDOWN_WORLDS) |
PluginSetupEvent | Void | Plugin setup completed |
PrepareUniverseEvent | Void | Universe preparation |
World Events
Section titled “World Events”| Event | Key Type | Cancellable | Description |
|---|---|---|---|
AddWorldEvent | String | Yes | World added to universe |
RemoveWorldEvent | String | Yes | World removed (cannot cancel if RemovalReason.EXCEPTIONAL) |
AllWorldsLoadedEvent | Void | No | All worlds finished loading |
StartWorldEvent | String | No | World started |
Player Events
Section titled “Player Events”| Event | Key Type | Async | Cancellable | Description |
|---|---|---|---|---|
PlayerConnectEvent | Void | No | No | Player connecting, can set initial world |
PlayerDisconnectEvent | Void | No | No | Player disconnected, provides DisconnectReason |
PlayerChatEvent | String | Yes | Yes | Player chat message with customizable formatter |
PlayerCraftEvent | String | No | No | Player crafting (deprecated) |
PlayerSetupConnectEvent | Void | No | Yes | Player setup phase connection |
PlayerSetupDisconnectEvent | Void | No | No | Player setup phase disconnection |
AddPlayerToWorldEvent | String | No | No | Player added to a world |
DrainPlayerFromWorldEvent | String | No | No | Player removed from a world |
PlayerReadyEvent | String | No | No | Player ready to play |
Entity Events
Section titled “Entity Events”| Event | Key Type | Description |
|---|---|---|
EntityRemoveEvent | Void | Entity removed from world |
LivingEntityInventoryChangeEvent | String | Living entity inventory changed |
LivingEntityUseBlockEvent | String | Living entity uses a block |
Block Events (ECS Events)
Section titled “Block Events (ECS Events)”| Event | Cancellable | Description |
|---|---|---|
PlaceBlockEvent | Yes | Block placed, provides ItemStack, Vector3i, and RotationTuple |
BreakBlockEvent | Yes | Block broken, provides ItemStack, Vector3i, and BlockType |
DamageBlockEvent | Yes | Block damaged |
UseBlockEvent | Yes | Block used/interacted with |
Other ECS Events
Section titled “Other ECS Events”| Event | Cancellable | Description |
|---|---|---|
CraftRecipeEvent | Yes | Recipe crafted |
DropItemEvent | Yes | Item dropped |
InteractivelyPickupItemEvent | Yes | Item picked up interactively |
SwitchActiveSlotEvent | Yes | Active slot switched |
ChangeGameModeEvent | Yes | Game mode changed |
DiscoverZoneEvent | Yes | Zone discovered |
Asset Events
Section titled “Asset Events”| Event | Key Type | Description |
|---|---|---|
LoadedAssetsEvent | Void | Assets loaded, provides asset map and query |
RemovedAssetsEvent | Void | Assets removed, indicates if replaced |
GenerateAssetsEvent | Void | Asset generation, implements IProcessedEvent |
Permission Events
Section titled “Permission Events”| Event | Key Type | Description |
|---|---|---|
GroupPermissionChangeEvent | Void | Group permission changed |
PlayerPermissionChangeEvent | Void | Player permission changed |
PlayerGroupEvent | Void | Player group changed |
Unregistering Event Handlers
Section titled “Unregistering Event Handlers”Event registrations can be removed when no longer needed:
EventRegistration<Void, BootEvent> registration = getEventRegistry().register(BootEvent.class, this::onBoot);
// Later, unregister when doneregistration.close();The EventRegistry provided by plugins automatically handles cleanup when the plugin is unloaded.
Async Event Handlers
Section titled “Async Event Handlers”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; }));Unhandled Event Handlers
Section titled “Unhandled Event Handlers”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); });Best Practices
Section titled “Best Practices”- Use appropriate priority - Don’t always use FIRST/LAST
- Check hasListener() - Avoid creating events when no one listens
- Handle async properly - Don’t block in async handlers
- Respect cancellation - Check isCancelled() before actions
- Use keyed events - For scoped/efficient event handling
- Clean exception handling - Exceptions are logged but don’t stop other handlers
- Use registerGlobal for cross-key listeners - When you need to handle all instances of a keyed event
- Prefer async events for I/O operations - Avoid blocking the main thread