Event API
import { eventWatch, eventIntercept, eventOff } from 'yeow-api';Subscribe: eventWatch(eventType, handler) (the default)
Read-only observation: it does not modify the event, and the runtime does not wait for it. Use it for almost everything (logging, metrics, forwarding, syncing state).
| Parameter | Type | Description |
|---|---|---|
eventType | string | Event type name |
handler | (e: Readonly<EventType>) => void | Handler (the event object is read-only) |
Returns a subscription handle: call it to unsubscribe; await sub.ready waits until the runtime has applied it.
const sub = eventWatch('playerJoin', async (e) => {
log.info(`${await e.player.getName()} joined`);
});
sub(); // unsubscribe
await sub.ready; // when "subscription is live" matters (triggering the event right after)Why this is the default: observers never make the event bridge spin and never push an event into its timeout. The event object is typed
Readonly, soe.cancelled = trueis a type error (use intercept).
Intercept: eventIntercept(eventType, handler) (opt-in)
Modify/cancel the event: the runtime waits for the handler to release it (event.complete); fields written are applied when the event completes.
| Parameter | Type | Description |
|---|---|---|
eventType | string | Event type name |
handler | (e: EventType) => void | Writable handler |
options.manualRelease | boolean | Release manually (async decisions) |
eventIntercept('blockBreak', (e) => {
if (e.block === 'minecraft:bedrock') e.cancelled = true;
});
// async decision: with manualRelease you receive complete()
eventIntercept('blockBreak', { manualRelease: true }, (e, complete) => {
fetchPermission(result => {
e.cancelled = !result.allowed;
complete({ cancelled: e.cancelled });
});
});Use sparingly: the handler runs inside the event dispatch window on the game thread and its writes are applied when the event completes. Returning a Promise means "no modification, release immediately" (use
manualReleasefor async decisions). Multiple interceptors run serially in registration order.
The two modes
Subscribe eventWatch | Intercept eventIntercept | |
|---|---|---|
| Can modify the event | No (read-only type) | Yes |
| Does the runtime wait | No — delivery continues | Yes — waits for event.complete |
| Can spin the game thread | No | Yes (5s timeout as backstop) |
| Use for | logging / metrics / forwarding / state sync | cancel, change messages, MOTD, permission checks |
eventOff(eventType, handler)
Unsubscribe/intercept removal (works for both modes). Pass the same handler reference.
const handler = (e) => { ... };
eventWatch('playerJoin', handler);
eventOff('playerJoin', handler);Event Parameters
In TS mode there are complete type definitions:
import type { PlayerJoinEvent, BlockBreakEvent } from 'yeow-api';
eventWatch('playerJoin', (e: PlayerJoinEvent) => {
console.log(e.player.name); // Player object
console.log(e.joinMessage);
});In JS mode event object field names match type table, player field automatically converts to Player object.
Event List
Event field value domains (interaction/click action
action, damage/teleport causecause, container typeinventoryTypeetc.) see Value Domain Appendix (action/container types see "Directly Maintained Enumeration List", damage/health regen causes see "Reference Implementation").
Player Events
| Type | Fields | Cancellable |
|---|---|---|
playerJoin | player, joinMessage | |
playerQuit | player, quitMessage | |
playerChat | player, message, format | ✔ |
playerMove | player, from, to | ✔ |
playerInteract | player, action, material, block | ✔ |
playerCommand | player, message | ✔ |
playerDeath | player, deathMessage(Message), deathType | ✔ |
playerRespawn | player, respawnLocation | |
playerTeleport | player, from, to, cause | ✔ |
playerItemConsume | player, item(ItemStack) | |
playerDropItem | player, item(ItemStack) | ✔ |
playerPickupItem | player, item(ItemStack) | ✔ |
playerBucketFill | player, bucket | ✔ |
playerBucketEmpty | player, bucket | ✔ |
playerExpChange | player, amount | |
playerLevelChange | player, oldLevel, newLevel | |
playerGameModeChange | player, newGameMode | ✔ |
playerAdvancementDone | player, advancement, title(Message)?, description(Message)? | |
playerToggleSneak | player, sneaking | |
playerToggleFlight | player, flying | |
foodLevelChange | player, oldFoodLevel, newFoodLevel | ✔ |
Entity Events
| Type | Fields | Cancellable |
|---|---|---|
entityDamage | entity, damage, cause, entityType | ✔ |
entityDeath | entity, entityType, entityName | |
entitySpawn | entity, entityType, location(Location) | ✔ |
entityExplode | entity, entityType, location(Location), blockCount | ✔ |
entityRegainHealth | entity, amount, reason | |
entityTarget | entity, target | ✔ |
projectileLaunch | entity, projectileType, shooter? | ✔ |
projectileHit | entity, projectileType, hitEntity?, hitBlock? |
World Events
| Type | Fields | Cancellable |
|---|---|---|
blockBreak | player, block, location(Location) | ✔ |
blockPlace | player, block, blockAgainst, location(Location) | ✔ |
blockFade | block, location(Location) | |
blockGrow | block, location(Location) | |
blockSpread | block, location(Location) | |
blockExplode | block, location(Location) | ✔ |
Inventory Events
| Type | Fields | Cancellable |
|---|---|---|
inventoryOpen | player, inventoryType, title | |
inventoryClose | player, inventoryType, inventoryId? | |
inventoryClick | player, slot, hotbarKey, action, inventoryType, isLeftClick, isRightClick, isShiftClick, clickedItem, cursorItem, inventoryId? | ✔ |
inventoryId: When event occurs on Yeow custom Inventory (
Inventory.createcreated), carries that Inventory's handle id (inventory.toString()) — in multi custom Inventory scenarios usee.inventoryId === inventory.toString()to identify click/close attribution. When not custom Inventory (inventory, chest etc.) it's absent.
Server Events
| Type | Fields | Cancellable |
|---|---|---|
serverPing | address, numPlayers, maxPlayers, motd | ✔ |
serverCommand | command, sender | ✔ |
playerResourcePackStatus | player, status, hash |
Permission checks are not events:
permissionCheckwas extracted from the event system into a dedicated request/response API,onPermissionCheck(no event object, nothing to cancel, and no observer mode).
Event Writeback
Write-back only applies to intercept (
eventIntercept). Subscribe (eventWatch) is read-only observation: field writes and return values are ignored.
Event results write back through three methods. Writeback only effective in handler's synchronous segment — returning Promise immediately releases event, modifications after await are all invalid (see constraints of each method below).
Method 1: Return Value (mods) — Auto Mode
Handler's return value merges into event writeback (mods):
eventIntercept('serverPing', (e) => {
return { motd: 'Hello!' }; // Merge writeback, modify server list MOTD
});Returning Promise treated as no modification, immediately released, doesn't wait for completion (async handler's async results won't writeback):
eventIntercept('blockBreak', async (e) => {
e.cancelled = true; // ✅ Synchronous segment effective
await fetchData(); // Event already released
return { cancelled: false }; // ❌ Invalid
});Method 2: Modify Event Parameters (event) — Auto Mode
In auto mode directly assigning event fields also writes back (merges with return value, direct assignment takes priority over return value; all fields except cancelled collected):
eventIntercept('playerDeath', (e) => {
e.deathMessage = { text: '§cA hero has fallen.' }; // Writeback death message (Message object or string)
});
eventIntercept('serverPing', (e) => {
e.motd = 'Hello!'; // Equivalent to return { motd }
});
eventIntercept('blockBreak', (e) => {
e.cancelled = true; // Cancel (cancellable event)
});TIP
The motd write-back in serverPing is not persisted: it only affects that ping response. Meanwhile setMotd writes to server.properties (survives restarts) — when you need a dynamic MOTD without overwriting the owner's configuration, use the serverPing write-back instead of setMotd.
Fields supporting writeback (runtime actually applies; other field assignments sent but runtime ignores — read-only fields):
Event Writeable Fields Description playerJoinjoinMessageJoin message playerQuitquitMessageQuit message playerChatmessage/formatChat content / format playerCommandmessageCommand string (including /)playerMove/playerTeleporttoTarget position: {x, y, z, yaw?, pitch?, world?}(world defaults to current world;from/causeread-only)playerRespawnrespawnLocationRespawn position (same shape as above) playerDeathdeathMessageMessage object {key, args}/{text}or stringfoodLevelChangenewFoodLevelNew food level entityDamagedamageDamage amount entityRegainHealthamountRecovery amount entityTargettargetTarget entity UUID or null(clear target)inventoryClickclickedItem/cursorItemClicked item / cursor item: {type, amount?}(cursorItem'samount: 0means clear cursor)serverPingmotd/maxPlayers/numPlayers/iconSee event table ( numPlayersrequires the dispatched instance to be Paper'sPaperServerListPingEvent— true on both platforms in practice, since Folia derives from Paper; only a base-classServerListPingEventmakes it read-only)All cancellable events cancelledCancel
Method 3: Manual Mode complete(mods)
After setting { manualRelease: true }, handler receives (event, complete), controls when to end event by calling complete(result). Suitable for scenarios needing to asynchronously obtain data before deciding result:
eventIntercept('blockBreak', { manualRelease: true }, (e, complete) => {
doAsyncCheck((result) => {
e.cancelled = result;
complete({ cancelled: result });
});
});complete only takes effect once, subsequent calls ignored.
Selection Guide
| Scenario | Recommended | Description |
|---|---|---|
| Only trigger logic (send message, log, API call) | Auto + async | Doesn't block main thread, freely use async API |
| Need to synchronously decide result (cancel, change death message, MOTD etc.) | Auto + sync handler | Directly assign event fields (or return mods) |
| Need to asynchronously obtain data before deciding result | Manual mode + complete() | User actively controls end timing |