hytale-events-api
SKILL.md
Hytale Events API
Complete guide for handling server events and creating custom event systems.
When to use this skill
Use this skill when:
- Listening for player actions (connect, chat, interact)
- Reacting to world changes (blocks, chunks)
- Handling entity events (damage, death, spawn)
- Creating custom events
- Managing event priorities and cancellation
- Implementing async event handlers
Event System Architecture
Hytale has two event systems:
- General Event Bus - Server-wide events (players, worlds, assets)
- ECS Event System - Entity-specific events (damage, interactions)
General Event Bus
Event Registration
Register in plugin setup():
@Override
protected void setup() {
// Global listener - receives ALL events of this type
getEventRegistry().registerGlobal(
PlayerConnectEvent.class,
this::onPlayerConnect
);
// Keyed listener - receives events with specific key
getEventRegistry().register(
AddPlayerToWorldEvent.class,
"world_name",
this::onPlayerAddToWorld
);
// Priority listener
getEventRegistry().registerGlobal(
EventPriority.FIRST,
SomeEvent.class,
this::onSomeEvent
);
// Unhandled listener - only if no keyed handler processed
getEventRegistry().registerUnhandled(
SomeEvent.class,
this::onUnhandledEvent
);
}
Event Priorities
public enum EventPriority {
FIRST((short)-21844), // Runs first
EARLY((short)-10922),
NORMAL((short)0), // Default
LATE((short)10922),
LAST((short)21844); // Runs last
}
Handlers execute in priority order. Use custom short values for fine-grained control.
Event Cancellation
For cancellable events:
private void onPlayerInteract(PlayerInteractEvent event) {
if (shouldBlock(event)) {
event.setCancelled(true);
}
}
Check cancellation:
private void onEvent(SomeEvent event) {
if (event instanceof ICancellable cancellable && cancellable.isCancelled()) {
return; // Already cancelled by earlier handler
}
// Process event
}
Player Events
Connection Events
// Player connecting (before entering world)
getEventRegistry().registerGlobal(PlayerConnectEvent.class, event -> {
Player player = event.getPlayer();
getLogger().atInfo().log("Player connecting: %s", player.getName());
});
// Player setup connect (cancellable)
getEventRegistry().registerGlobal(PlayerSetupConnectEvent.class, event -> {
if (isBanned(event.getPlayer())) {
event.setCancelled(true);
event.setDisconnectReason("You are banned!");
}
});
// Player disconnecting
getEventRegistry().registerGlobal(PlayerDisconnectEvent.class, event -> {
Player player = event.getPlayer();
savePlayerData(player);
});
// Player added to world
getEventRegistry().register(AddPlayerToWorldEvent.class, "main", event -> {
event.getPlayer().sendMessage("Welcome to the main world!");
});
// Player ready (fully loaded)
getEventRegistry().registerGlobal(PlayerReadyEvent.class, event -> {
Player player = event.getPlayer();
showWelcomeScreen(player);
});
Chat Event
// Async event - returns CompletableFuture
getEventRegistry().registerAsyncGlobal(
PlayerChatEvent.class,
future -> future.thenApply(event -> {
String message = event.getMessage();
// Modify message
event.setMessage("[Custom] " + message);
// Or cancel
if (containsBadWords(message)) {
event.setCancelled(true);
}
return event;
})
);
Interaction Events
getEventRegistry().registerGlobal(PlayerInteractEvent.class, event -> {
Player player = event.getPlayer();
InteractionType type = event.getInteractionType();
switch (type) {
case USE -> handleUse(event);
case ATTACK -> handleAttack(event);
case LOOK -> handleLook(event);
}
});
World Events
World Lifecycle
// World being added (cancellable)
getEventRegistry().register(AddWorldEvent.class, "new_world", event -> {
World world = event.getWorld();
initializeWorld(world);
});
// World being removed (cancellable except EXCEPTIONAL)
getEventRegistry().register(RemoveWorldEvent.class, "old_world", event -> {
if (event.getReason() != RemoveWorldEvent.Reason.EXCEPTIONAL) {
saveWorldData(event.getWorld());
}
});
// World started
getEventRegistry().register(StartWorldEvent.class, "main", event -> {
spawnInitialEntities(event.getWorld());
});
// All worlds loaded
getEventRegistry().registerGlobal(AllWorldsLoadedEvent.class, event -> {
getLogger().atInfo().log("All worlds ready!");
});
Chunk Events
// Chunk pre-load processing
getEventRegistry().registerGlobal(
EventPriority.FIRST,
ChunkPreLoadProcessEvent.class,
event -> {
// Modify chunk before it's fully loaded
Chunk chunk = event.getChunk();
}
);
Entity Events
Entity Removal
getEventRegistry().register(EntityRemoveEvent.class, "world_name", event -> {
Entity entity = event.getEntity();
if (entity instanceof NPCEntity npc) {
logNPCRemoval(npc);
}
});
Inventory Events
getEventRegistry().register(
LivingEntityInventoryChangeEvent.class,
"world_name",
event -> {
LivingEntity entity = event.getEntity();
ItemStack oldItem = event.getOldItem();
ItemStack newItem = event.getNewItem();
int slot = event.getSlot();
}
);
ECS Events
For entity-specific events, use the ECS event system.
Block Events
@Override
protected void setup() {
// Register event systems
getEntityStoreRegistry().registerSystem(new BreakBlockHandler());
getEntityStoreRegistry().registerSystem(new PlaceBlockHandler());
getEntityStoreRegistry().registerSystem(new UseBlockHandler());
}
public class BreakBlockHandler extends EntityEventSystem<EntityStore, BreakBlockEvent> {
public BreakBlockHandler() {
super(BreakBlockEvent.class);
}
@Override
public void handle(
int index,
ArchetypeChunk<EntityStore> chunk,
Store<EntityStore> store,
CommandBuffer<EntityStore> buffer,
BreakBlockEvent event
) {
BlockType blockType = event.getBlockType();
Vector3i position = event.getPosition();
Player player = event.getPlayer();
// Cancel if protected
if (isProtected(position)) {
event.setCancelled(true);
player.sendMessage("This area is protected!");
return;
}
// Custom drops
if (blockType.getId().equals("MyPlugin:CustomOre")) {
event.setDrops(createCustomDrops());
}
}
}
Place Block Event
public class PlaceBlockHandler extends EntityEventSystem<EntityStore, PlaceBlockEvent> {
public PlaceBlockHandler() {
super(PlaceBlockEvent.class);
}
@Override
public void handle(..., PlaceBlockEvent event) {
BlockType blockType = event.getBlockType();
Vector3i position = event.getPosition();
// Prevent placing in certain areas
if (isRestrictedArea(position)) {
event.setCancelled(true);
}
}
}
Use Block Event
public class UseBlockHandler extends EntityEventSystem<EntityStore, UseBlockEvent.Pre> {
public UseBlockHandler() {
super(UseBlockEvent.Pre.class);
}
@Override
public void handle(..., UseBlockEvent.Pre event) {
BlockType blockType = event.getBlockType();
Player player = event.getPlayer();
// Custom interaction
if (blockType.getId().equals("MyPlugin:TeleportBlock")) {
teleportPlayer(player);
event.setCancelled(true); // Prevent default behavior
}
}
}
Damage Event
public class DamageHandler extends EntityEventSystem<EntityStore, Damage> {
private ComponentAccess<EntityStore, TransformComponent> transforms;
public DamageHandler() {
super(Damage.class);
}
@Override
protected void register(Store<EntityStore> store) {
transforms = registerComponent(TransformComponent.class);
}
@Override
public void handle(..., Damage event) {
float amount = event.getAmount();
DamageCause cause = event.getCause();
Entity source = event.getSource();
// Modify damage
if (isInSafeZone(transforms.get(chunk, index).getPosition())) {
event.setAmount(0);
event.setCancelled(true);
}
// Increase damage for critical
if (event.isCritical()) {
event.setAmount(amount * 1.5f);
}
}
}
Item Events
// Drop item
public class DropHandler extends EntityEventSystem<EntityStore, DropItemEvent> {
public DropHandler() {
super(DropItemEvent.class);
}
@Override
public void handle(..., DropItemEvent event) {
ItemStack item = event.getItemStack();
if (isSoulbound(item)) {
event.setCancelled(true);
}
}
}
// Pickup item
public class PickupHandler extends EntityEventSystem<EntityStore, InteractivelyPickupItemEvent> {
public PickupHandler() {
super(InteractivelyPickupItemEvent.class);
}
@Override
public void handle(..., InteractivelyPickupItemEvent event) {
ItemStack item = event.getItemStack();
Entity entity = event.getEntity();
if (!canPickup(entity, item)) {
event.setCancelled(true);
}
}
}
Craft Event
public class CraftHandler extends EntityEventSystem<EntityStore, CraftRecipeEvent> {
public CraftHandler() {
super(CraftRecipeEvent.class);
}
@Override
public void handle(..., CraftRecipeEvent event) {
CraftingRecipe recipe = event.getRecipe();
Player player = event.getPlayer();
// Check permissions
if (!hasRecipeUnlocked(player, recipe)) {
event.setCancelled(true);
player.sendMessage("Recipe not unlocked!");
}
}
}
Asset Events
Asset Loading
getEventRegistry().register(
LoadedAssetsEvent.class,
BlockType.class,
event -> {
for (BlockType block : event.getLoadedAssets()) {
processBlockType(block);
}
}
);
getEventRegistry().register(
LoadedAssetsEvent.class,
Item.class,
event -> {
for (Item item : event.getLoadedAssets()) {
processItem(item);
}
}
);
Asset Removal
getEventRegistry().register(
RemovedAssetsEvent.class,
BlockType.class,
event -> {
for (String removedId : event.getRemovedIds()) {
cleanupBlockType(removedId);
}
}
);
Asset Pack Events
getEventRegistry().registerGlobal(AssetPackRegisterEvent.class, event -> {
AssetPack pack = event.getAssetPack();
getLogger().atInfo().log("Asset pack registered: %s", pack.getName());
});
getEventRegistry().registerGlobal(AssetPackUnregisterEvent.class, event -> {
AssetPack pack = event.getAssetPack();
getLogger().atInfo().log("Asset pack unregistered: %s", pack.getName());
});
Plugin Events
// When another plugin completes setup
getEventRegistry().register(
PluginSetupEvent.class,
OtherPlugin.class,
event -> {
OtherPlugin plugin = (OtherPlugin) event.getPlugin();
integrateWith(plugin);
}
);
Creating Custom Events
Simple Event
public class MyCustomEvent implements IEvent<Void> {
private final Player player;
private final String data;
public MyCustomEvent(Player player, String data) {
this.player = player;
this.data = data;
}
public Player getPlayer() { return player; }
public String getData() { return data; }
}
// Dispatch the event
HytaleServer.get().getEventBus()
.dispatchFor(MyCustomEvent.class)
.dispatch(new MyCustomEvent(player, "some data"));
Keyed Event
public class MyWorldEvent implements IEvent<String> {
private final World world;
public MyWorldEvent(World world) {
this.world = world;
}
public World getWorld() { return world; }
}
// Dispatch with key
HytaleServer.get().getEventBus()
.dispatchFor(MyWorldEvent.class, world.getName())
.dispatch(new MyWorldEvent(world));
Cancellable Event
public class MyCancellableEvent implements IEvent<Void>, ICancellable {
private boolean cancelled = false;
private final Player player;
public MyCancellableEvent(Player player) {
this.player = player;
}
public Player getPlayer() { return player; }
@Override
public boolean isCancelled() { return cancelled; }
@Override
public void setCancelled(boolean cancelled) {
this.cancelled = cancelled;
}
}
// Dispatch and check
MyCancellableEvent event = HytaleServer.get().getEventBus()
.dispatchFor(MyCancellableEvent.class)
.dispatch(new MyCancellableEvent(player));
if (!event.isCancelled()) {
// Proceed with action
}
Async Event
public class MyAsyncEvent implements IAsyncEvent<Void>, ICancellable {
private boolean cancelled = false;
private final Player player;
private String result;
public MyAsyncEvent(Player player) {
this.player = player;
}
public Player getPlayer() { return player; }
public String getResult() { return result; }
public void setResult(String result) { this.result = result; }
@Override
public boolean isCancelled() { return cancelled; }
@Override
public void setCancelled(boolean cancelled) {
this.cancelled = cancelled;
}
}
// Dispatch async
HytaleServer.get().getEventBus()
.dispatchForAsync(MyAsyncEvent.class)
.dispatch(new MyAsyncEvent(player))
.thenAccept(event -> {
if (!event.isCancelled()) {
processResult(event.getResult());
}
});
Custom ECS Event
public class MyEntityEvent extends CancellableEcsEvent {
private final ItemStack item;
private float modifier = 1.0f;
public MyEntityEvent(ItemStack item) {
this.item = item;
}
public ItemStack getItem() { return item; }
public float getModifier() { return modifier; }
public void setModifier(float modifier) { this.modifier = modifier; }
}
// Register event type
EntityEventType<EntityStore, MyEntityEvent> eventType =
getEntityStoreRegistry().registerEntityEventType(MyEntityEvent.class);
// Invoke on entity
store.invoke(entityRef, new MyEntityEvent(itemStack));
Event Best Practices
Performance
// Check for listeners before creating event
IEventDispatcher dispatcher = HytaleServer.get().getEventBus()
.dispatchFor(ExpensiveEvent.class);
if (dispatcher.hasListener()) {
dispatcher.dispatch(new ExpensiveEvent(computeExpensiveData()));
}
Cleanup
Event registrations are automatically cleaned up when plugin disables. For manual cleanup:
private EventRegistration registration;
@Override
protected void setup() {
registration = getEventRegistry().registerGlobal(
SomeEvent.class,
this::handler
);
}
public void disableFeature() {
if (registration != null) {
registration.unregister();
registration = null;
}
}
Error Handling
private void onPlayerConnect(PlayerConnectEvent event) {
try {
processPlayer(event.getPlayer());
} catch (Exception e) {
getLogger().atSevere().withCause(e).log("Error processing player connect");
// Don't rethrow - let other handlers run
}
}
Troubleshooting
Events Not Firing
- Verify registration is in
setup() - Check event class matches exactly
- Verify key matches for keyed events
- Ensure plugin is enabled
Handler Not Called
- Check priority vs other handlers
- Verify event not cancelled by earlier handler
- Check for exceptions in handler
Async Events Hanging
- Always complete the CompletableFuture
- Add timeout handling
- Check for deadlocks
See references/event-list.md for complete event catalog.
See references/ecs-events.md for ECS event patterns.