Skip to content

Creating Custom Events

This guide covers how to define your own event types for use in the Hytale event system.

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

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

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

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

Any type with proper equals() and hashCode() can be used.

public class MinigameEvent implements IEvent<MinigameId> { }

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 + "}";
}
}

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 + "}";
}
}

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);

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;
}
}

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 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:

@Override
public void setup() {
this.getEntityStoreRegistry().registerSystem(new BlockInteractionHandler());
}
// dispatch from anywhere with store access
CustomBlockInteractionEvent 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.

  • Immutable fields: Use final for data that identifies the event (player, position, etc.)
  • Mutable fields: Allow modification for data handlers might change (destination, message, targets)
// immutable - identifies the event
private final PlayerRef player;
private final Vector3i originalPosition;
// mutable - handlers can modify
private Vector3d destination;
private String message;
private List<PlayerRef> targets;

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;
// ...
}

Always implement toString() for debugging:

@Nonnull
@Override
public String toString() {
return "MyEvent{" +
"player=" + player +
", data=" + data +
", cancelled=" + cancelled +
'}';
}

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 {
// ...
}
Event TypeInterfaceUse Case
Sync, globalIEvent<Void>Server events, global actions
Sync, keyedIEvent<String>World-specific, channel-specific
Sync, type-keyedIEvent<Class<?>>Asset events, type-filtered
Async, globalIAsyncEvent<Void>I/O operations, database
CancellableAdd ICancellableActions that can be prevented
ECSExtend EcsEvent / CancellableEcsEventBlock/entity interactions