Creating Custom Events
This guide covers how to define your own event types for use in the Hytale event system.
Choosing the Right Interface
Section titled “Choosing the Right Interface”IEvent<KeyType> - Synchronous Events
Section titled “IEvent<KeyType> - Synchronous Events”Use for events that should be processed immediately and sequentially. Most game events use this.
public class MyEvent implements IEvent<Void> { }Use when:
- Handlers need to run in a specific order
- You need to check
isCancelled()immediately after dispatch - The event is triggered from game tick logic
IAsyncEvent<KeyType> - Asynchronous Events
Section titled “IAsyncEvent<KeyType> - Asynchronous Events”Use for events where handlers may perform I/O operations, database queries, or other async work.
public class MyAsyncEvent implements IAsyncEvent<Void> { }Use when:
- Handlers might make network calls or database queries
- Processing can happen in parallel
- You want to chain async operations via
CompletableFuture
Choosing the KeyType
Section titled “Choosing the KeyType”Void - Global Events
Section titled “Void - Global Events”All registered listeners receive the event. No filtering.
public class ServerReadyEvent implements IEvent<Void> { }Use when:
- The event is truly global (server boot, shutdown)
- There’s no logical grouping or filtering needed
String - Named Key Events
Section titled “String - Named Key Events”Events can be filtered by a string identifier (world name, channel, etc.).
public class ZoneEnterEvent implements IEvent<String> { }Use when:
- Events are world-specific
- Events belong to named channels or categories
- You want listeners to subscribe to specific instances
Class<?> - Type Key Events
Section titled “Class<?> - Type Key Events”Events can be filtered by a class type.
public class AssetLoadedEvent<T> implements IEvent<Class<T>> { }Use when:
- Events relate to specific asset or data types
- You want type-safe filtering
Custom KeyType
Section titled “Custom KeyType”Any type with proper equals() and hashCode() can be used.
public class MinigameEvent implements IEvent<MinigameId> { }Event Examples
Section titled “Event Examples”Basic Event (Void Key)
Section titled “Basic Event (Void Key)”A simple global event with immutable data:
package com.example.events;
import com.hypixel.hytale.event.IEvent;import javax.annotation.Nonnull;
public class ServerReadyEvent implements IEvent<Void> {
private final long startupTimeMs;
public ServerReadyEvent(long startupTimeMs) { this.startupTimeMs = startupTimeMs; }
public long getStartupTimeMs() { return this.startupTimeMs; }
@Nonnull @Override public String toString() { return "ServerReadyEvent{startupTimeMs=" + this.startupTimeMs + "}"; }}Cancellable Event
Section titled “Cancellable Event”An event that handlers can cancel to prevent the action from occurring:
package com.example.events;
import com.hypixel.hytale.event.IEvent;import com.hypixel.hytale.event.ICancellable;import com.hypixel.hytale.server.core.universe.PlayerRef;import com.hypixel.hytale.math.Vector3d;import javax.annotation.Nonnull;
public class PlayerTeleportEvent implements IEvent<Void>, ICancellable {
@Nonnull private final PlayerRef player; @Nonnull private Vector3d destination; private boolean cancelled;
public PlayerTeleportEvent(@Nonnull PlayerRef player, @Nonnull Vector3d destination) { this.player = player; this.destination = destination; this.cancelled = false; }
@Nonnull public PlayerRef getPlayer() { return this.player; }
@Nonnull public Vector3d getDestination() { return this.destination; }
/** * Allows handlers to modify the destination. */ public void setDestination(@Nonnull Vector3d destination) { this.destination = destination; }
@Override public boolean isCancelled() { return this.cancelled; }
@Override public void setCancelled(boolean cancelled) { this.cancelled = cancelled; }
@Nonnull @Override public String toString() { return "PlayerTeleportEvent{player=" + player + ", destination=" + destination + ", cancelled=" + cancelled + "}"; }}Keyed Event (String)
Section titled “Keyed Event (String)”An event filtered by world name:
package com.example.events;
import com.hypixel.hytale.event.IEvent;import com.hypixel.hytale.server.core.universe.world.World;import com.hypixel.hytale.server.core.universe.PlayerRef;import javax.annotation.Nonnull;
public class ZoneEnterEvent implements IEvent<String> {
@Nonnull private final World world; @Nonnull private final PlayerRef player; @Nonnull private final String zoneName;
public ZoneEnterEvent(@Nonnull World world, @Nonnull PlayerRef player, @Nonnull String zoneName) { this.world = world; this.player = player; this.zoneName = zoneName; }
@Nonnull public World getWorld() { return this.world; }
@Nonnull public PlayerRef getPlayer() { return this.player; }
@Nonnull public String getZoneName() { return this.zoneName; }
@Nonnull @Override public String toString() { return "ZoneEnterEvent{world=" + world.getName() + ", player=" + player + ", zone=" + zoneName + "}"; }}Dispatch with key:
eventBus.dispatchFor(ZoneEnterEvent.class, world.getName()) .dispatch(new ZoneEnterEvent(world, player, zoneName));Listen for specific world:
events.register(ZoneEnterEvent.class, "adventure_world", this::onZoneEnter);Listen for all worlds:
events.registerGlobal(ZoneEnterEvent.class, this::onAnyZoneEnter);Async Event
Section titled “Async Event”An event for asynchronous processing:
package com.example.events;
import com.hypixel.hytale.event.IAsyncEvent;import com.hypixel.hytale.event.ICancellable;import com.hypixel.hytale.server.core.universe.PlayerRef;import javax.annotation.Nonnull;import java.util.List;
public class AsyncMessageBroadcastEvent implements IAsyncEvent<Void>, ICancellable {
@Nonnull private String message; @Nonnull private List<PlayerRef> recipients; private boolean cancelled;
public AsyncMessageBroadcastEvent(@Nonnull String message, @Nonnull List<PlayerRef> recipients) { this.message = message; this.recipients = recipients; this.cancelled = false; }
@Nonnull public String getMessage() { return this.message; }
public void setMessage(@Nonnull String message) { this.message = message; }
@Nonnull public List<PlayerRef> getRecipients() { return this.recipients; }
public void setRecipients(@Nonnull List<PlayerRef> recipients) { this.recipients = recipients; }
@Override public boolean isCancelled() { return this.cancelled; }
@Override public void setCancelled(boolean cancelled) { this.cancelled = cancelled; }}Abstract Base Event Class
Section titled “Abstract Base Event Class”Create a base class for related events:
package com.example.events;
import com.hypixel.hytale.event.IEvent;import javax.annotation.Nonnull;
/** * Base class for all minigame events, keyed by minigame ID. */public abstract class MinigameEvent implements IEvent<String> {
@Nonnull private final String minigameId;
public MinigameEvent(@Nonnull String minigameId) { this.minigameId = minigameId; }
@Nonnull public String getMinigameId() { return this.minigameId; }}Concrete implementations:
package com.example.events;
import com.hypixel.hytale.server.core.universe.PlayerRef;import javax.annotation.Nonnull;import java.util.List;
public class MinigameStartEvent extends MinigameEvent {
@Nonnull private final List<PlayerRef> participants;
public MinigameStartEvent(@Nonnull String minigameId, @Nonnull List<PlayerRef> participants) { super(minigameId); this.participants = participants; }
@Nonnull public List<PlayerRef> getParticipants() { return this.participants; }}
public class MinigameEndEvent extends MinigameEvent {
@Nonnull private final PlayerRef winner; private final int durationSeconds;
public MinigameEndEvent(@Nonnull String minigameId, @Nonnull PlayerRef winner, int durationSeconds) { super(minigameId); this.winner = winner; this.durationSeconds = durationSeconds; }
@Nonnull public PlayerRef getWinner() { return this.winner; }
public int getDurationSeconds() { return this.durationSeconds; }}ECS Events
Section titled “ECS Events”ECS events are dispatched through the ECS store, not the event bus. Creating a custom ECS event involves three parts: the event class, a handler system, and registration.
1. Define the event — extend EcsEvent (or CancellableEcsEvent if cancellable):
public class CustomBlockInteractionEvent extends CancellableEcsEvent { @Nonnull private final Vector3i blockPosition; private final String interactionType;
public CustomBlockInteractionEvent(@Nonnull Vector3i blockPosition, String interactionType) { this.blockPosition = blockPosition; this.interactionType = interactionType; }
@Nonnull public Vector3i getBlockPosition() { return this.blockPosition; }
public String getInteractionType() { return this.interactionType; }}2. Create a handler — extend EntityEventSystem for entity-scoped events, or WorldEventSystem for world-scoped events:
public class BlockInteractionHandler extends EntityEventSystem<EntityStore, CustomBlockInteractionEvent> {
public BlockInteractionHandler() { super(CustomBlockInteractionEvent.class); }
@Override public void handle(int index, ArchetypeChunk<EntityStore> archetypeChunk, Store<EntityStore> store, CommandBuffer<EntityStore> commandBuffer, CustomBlockInteractionEvent event) { Player player = archetypeChunk.getComponent(index, Player.getComponentType()); // handle the interaction }
@Override public Query<EntityStore> getQuery() { return Player.getComponentType(); }}3. Register and dispatch — register the handler in setup(), then dispatch with store.invoke:
@Overridepublic void setup() { this.getEntityStoreRegistry().registerSystem(new BlockInteractionHandler());}
// dispatch from anywhere with store accessCustomBlockInteractionEvent event = new CustomBlockInteractionEvent(pos, "activate");store.invoke(entityRef, event);if (event.isCancelled()) { return;}See ECS Events for the full guide on handler types, query filtering, and cancellation.
Best Practices
Section titled “Best Practices”Field Mutability
Section titled “Field Mutability”- Immutable fields: Use
finalfor data that identifies the event (player, position, etc.) - Mutable fields: Allow modification for data handlers might change (destination, message, targets)
// immutable - identifies the eventprivate final PlayerRef player;private final Vector3i originalPosition;
// mutable - handlers can modifyprivate Vector3d destination;private String message;private List<PlayerRef> targets;Annotations
Section titled “Annotations”Use @Nonnull and @Nullable to clarify null safety:
import javax.annotation.Nonnull;import javax.annotation.Nullable;
public class MyEvent implements IEvent<Void> {
@Nonnull private final PlayerRef player;
@Nullable private String optionalData;
// ...}toString() Implementation
Section titled “toString() Implementation”Always implement toString() for debugging:
@Nonnull@Overridepublic String toString() { return "MyEvent{" + "player=" + player + ", data=" + data + ", cancelled=" + cancelled + '}';}Documentation
Section titled “Documentation”Document when the event is fired and what cancelling does:
/** * Fired when a player attempts to teleport. * * <p>Handlers can: * <ul> * <li>Cancel the teleport via {@link #setCancelled(boolean)}</li> * <li>Modify the destination via {@link #setDestination(Vector3d)}</li> * </ul> * * <p>Fired from: TeleportManager.teleportPlayer() */public class PlayerTeleportEvent implements IEvent<Void>, ICancellable { // ...}Summary
Section titled “Summary”| Event Type | Interface | Use Case |
|---|---|---|
| Sync, global | IEvent<Void> | Server events, global actions |
| Sync, keyed | IEvent<String> | World-specific, channel-specific |
| Sync, type-keyed | IEvent<Class<?>> | Asset events, type-filtered |
| Async, global | IAsyncEvent<Void> | I/O operations, database |
| Cancellable | Add ICancellable | Actions that can be prevented |
| ECS | Extend EcsEvent / CancellableEcsEvent | Block/entity interactions |