Skip to content
BasaltSoftware
Bedrock 1.26.60

Site colour

Discord
Esc

    ↑↓ moveEnter openEsc close

    Keyboard shortcuts
    Ctrl K
    Search the whole site, from any page
    /
    Jump to the sidebar search, or search the whole site where there isn't one
    Enter
    Open the top search result
    Esc
    Clear the search, or close this panel
    ?
    Show these shortcuts

    Events are how a plugin finds out what is happening. You subscribe a function to an event, and Basalt calls it each time the event fires.

    Events live in three places:

    Object What its events cover
    world Players, entities, blocks, items and weather. Mostly Mojang’s events, plus Basalt’s own.
    system The plugin’s own life cycle, script events and packets.
    server Basalt-only events about joining, commands and deaths. server is exported by @minecraft/server on Basalt and does not exist on BDS.

    Every event has subscribe and unsubscribe. subscribe returns the function you passed, so you can keep it to unsubscribe later.

    import { world } from "@minecraft/server";
    const onChat = world.afterEvents.chatSend.subscribe((event) => {
    console.log(`<${event.sender.name}> ${event.message}`);
    });
    // Later, to stop listening:
    world.afterEvents.chatSend.unsubscribe(onChat);

    An error thrown in a handler is logged and does not affect other handlers or other plugins. One error is not logged: an InvalidEntityError from a player or entity that left while the handler ran. The handler just stops there.

    Most things that happen have two events.

    After events (world.afterEvents) fire once the thing has happened. The game has already moved on, so you cannot change the outcome, but you are free to read and change anything.

    Before events (world.beforeEvents, server.beforeEvents) fire while the game waits for your answer. You can cancel the action with event.cancel = true, and some events let you change a value, such as the chat message or the damage.

    import { world } from "@minecraft/server";
    const banned = ["badword", "worseword"];
    world.beforeEvents.chatSend.subscribe((event) => {
    if (banned.some((word) => event.message.toLowerCase().includes(word))) {
    event.cancel = true;
    event.sender.sendMessage("§cWatch your language.");
    return;
    }
    // Change what everyone sees.
    event.message = event.message.replaceAll(":)", "☺");
    });

    Because the game is waiting, a before handler must be quick:

    • Basalt waits at most 50 ms for a handler. One that has not answered by then is no longer waited for, so its cancel or changes may come too late to count.
    • A handler that takes more than 10 ms earns the plugin a warning in the log and in /status.

    Do the minimum in the handler and move anything slow into system.run.

    This is the one rule that catches people out.

    Normally, when a plugin reads something (player.location, dimension.getBlock(...), an inventory slot), it asks the game and waits for the answer. Inside a before event the game is already waiting for the plugin, so it cannot answer. Basalt handles the two cases differently:

    Where What happens when you read
    Block break, block place and mob hurt before events Reading the world throws: blocks, entities, anything through dimension.
    Player before events and custom command callbacks A player is read as the plugin last saw them. If the plugin has never read that player before, the read throws.

    What always works inside any before event:

    • The event’s own fields. event.message, event.damage, event.block.typeId, event.block.location, event.player.name and so on are delivered with the event.
    • Changes. sendMessage, teleport, applyKnockback, playSound and other changes are sent without waiting.
    • Your own data. Variables, Maps and Sets in your plugin.

    When you need a real read, defer it with system.run. The callback runs on the next tick, outside the event, where every read works:

    import { system, world, GameMode } from "@minecraft/server";
    world.beforeEvents.playerBreakBlock.subscribe((event) => {
    const { player, block } = event;
    // Fine: these fields come with the event.
    if (block.typeId !== "minecraft:diamond_ore") return;
    const { x, y, z } = block.location;
    // Not here: player.getGameMode() or block.above() could throw.
    system.run(() => {
    if (!player.isValid) return;
    if (player.getGameMode() === GameMode.Survival) {
    world.sendMessage(`${player.name} found diamonds at ${x}, ${y}, ${z}`);
    }
    });
    });

    If the decision to cancel depends on something you would have to read, keep that state in your plugin and update it from after events:

    import { world } from "@minecraft/server";
    const muted = new Set<string>();
    world.afterEvents.playerSpawn.subscribe(({ player, initialSpawn }) => {
    // After events can read freely.
    if (initialSpawn && player.hasTag("muted")) muted.add(player.name);
    });
    world.afterEvents.playerLeave.subscribe(({ playerName }) => muted.delete(playerName));
    world.beforeEvents.chatSend.subscribe((event) => {
    // No read needed: the answer is already in the plugin.
    if (muted.has(event.sender.name)) event.cancel = true;
    });
    import { world } from "@minecraft/server";
    const spawnRadius = 32;
    function nearSpawn(location: { x: number; z: number }): boolean {
    return Math.abs(location.x) <= spawnRadius && Math.abs(location.z) <= spawnRadius;
    }
    world.beforeEvents.playerBreakBlock.subscribe((event) => {
    if (nearSpawn(event.block.location)) event.cancel = true;
    });
    world.beforeEvents.playerPlaceBlock.subscribe((event) => {
    if (nearSpawn(event.block.location)) event.cancel = true;
    });
    import { world, Player } from "@minecraft/server";
    world.beforeEvents.entityHurt.subscribe((event) => {
    // Players take half damage from everything.
    if (event.hurtEntity instanceof Player) event.damage = event.damage / 2;
    });

    For players, entityHurt also has knockback, which you can change, and cancelKnockback.

    server.beforeEvents.beforeJoin fires before the player is in the world, so there is no Player yet: the event carries the name, XUID and address instead.

    import { server } from "@minecraft/server";
    const maintenance = true;
    const staff = new Set(["Steve", "Alex"]);
    server.beforeEvents.beforeJoin.subscribe((event) => {
    if (maintenance && !staff.has(event.name)) {
    event.kickMessage = "Down for maintenance. Back soon!";
    event.cancel = true;
    }
    });
    import { server } from "@minecraft/server";
    server.beforeEvents.playerDie.subscribe((event) => {
    event.keepInventory = true;
    event.deathMessage = `${event.player.name} had a bad day`;
    });

    world.afterEvents.worldLoad fires on the first tick after the plugin has started, once the top-level code of every module has run. It is the place for one-time setup such as creating scoreboard objectives.

    import { world } from "@minecraft/server";
    world.afterEvents.worldLoad.subscribe(() => {
    console.log(`World ready, ${world.getAllPlayers().length} players online`);
    });

    world.beforeEvents.worldLoad (Basalt’s own) fires on the same tick, just before it. The old name worldInitialize works for both, and logs a deprecation warning.

    Events marked Basalt do not exist in Mojang’s API. Fields marked changeable can be assigned in the before event.

    Event Before After Fields
    world chatSend Yes Yes sender, message (changeable), cancel
    world playerJoin Yes playerId, playerName
    world playerSpawn Yes player, initialSpawn, location
    world playerLeave Yes Yes Before: player. After: playerId, playerName
    server beforeJoin (Basalt) Yes name, xuid, address, kickMessage (changeable), cancel
    server playerCommand (Basalt) Yes player, command (changeable), cancel
    Event Before After Fields
    world entityHurt Yes Yes hurtEntity, damage (changeable), damageSource, cancel; for players also knockback (changeable) and cancelKnockback
    world playerHurt (Basalt) Yes Yes The same as entityHurt, for players only
    world entityDie Yes deadEntity, damageSource
    server playerDie (Basalt) Yes player, damageSource, keepInventory, drops, deathMessage (all three changeable)
    server entityDie (Basalt) Yes deadEntity, damageSource, drops (changeable)
    world effectAdd Yes Yes Before: entity, effectType, duration (changeable), cancel. After: entity, effect
    world entityHitEntity Yes damagingEntity, hitEntity: a melee hit landing, on a mob or a player
    world entityHitBlock Yes damagingEntity, hitBlock, blockFace: a player’s attack landing on a block. hitBlockPermutation is not sent.
    world entityHealthChanged Yes entity, oldValue, newValue. Fires for players, and for mobs when they take damage.
    world projectileHitBlock, projectileHitEntity Yes projectile, source, location, hitVector, and getBlockHit() or getEntityHit()
    Event Before After Fields
    world playerBreakBlock Yes Yes player, block, cancel. After: brokenBlockPermutation
    world playerPlaceBlock Yes Yes player, block, permutationBeingPlaced, cancel
    world playerInteractWithBlock Yes Yes player, block, blockFace, itemStack, isFirstEvent, cancel
    world explosion Yes Yes dimension, source, getImpactedBlocks(). Before: setImpactedBlocks(), cancel
    world blockExplode Yes block, dimension, explodedBlockPermutation, source
    world weatherChange Yes Yes Before: previousWeather, newWeather and duration (both changeable), cancel
    world buttonPush, leverAction, pressurePlatePush, pressurePlatePop, targetBlockHit, tripWireTrip, pistonActivate Yes block, dimension and event-specific fields. The fields naming who pressed, stepped or shot are empty.
    world worldLoad Yes (Basalt) Yes Before: itemComponentRegistry. After: none
    Event Before After Fields
    world itemUse Yes Yes source, itemStack, cancel
    world itemStartUse, itemCompleteUse, itemReleaseUse, itemStopUse Yes source, itemStack, useDuration: eating, drinking, and drawing a bow or trident
    world playerPickupItem (Basalt) Yes Yes player, entity, itemStack, cancel
    world playerDropItem (Basalt) Yes Yes player, itemStack, cancel
    world itemSpawn (Basalt) Yes entity, itemStack, location
    world itemComponentUse (Basalt) Yes componentId, source, itemStack: a player used an item with a custom component. itemComponentRegistry uses it to run onUse.
    world playerHotbarSelectedSlotChange Yes player, newSlotSelected, previousSlotSelected, itemStack
    Event Before After Fields
    world playerInteractWithEntity Yes Yes player, target, itemStack, cancel
    world playerGameModeChange Yes Yes player, fromGameMode, toGameMode, cancel
    world playerMove (Basalt) Yes Yes player, fromLocation, toLocation (changeable), cancel puts them back
    world playerTeleport (Basalt) Yes Yes player, fromLocation, toLocation (changeable), cause, cancel
    world playerRespawn (Basalt) Yes Yes player, location (changeable before)
    world playerToggleSneak, playerToggleSprint (Basalt) Yes player, and isSneaking or isSprinting
    world playerEmote Yes (Basalt) Yes player, personaPieceId; cancelling hides the emote from others
    world playerChangeSkin (Basalt) Yes Yes player, skin, isTrusted; cancelling keeps the old skin
    world playerViolation (Basalt) Yes Yes player, check, detail; cancelling stops the anti-cheat kick
    world playerFormResponded (Basalt) Yes player, formId, response, cancelationReason
    Event Before After Fields
    world entitySpawn Yes entity, cause
    world entityRemove Yes Yes Before: removedEntity. After: removedEntityId, typeId
    Event Before After Fields
    system startup Yes customCommandRegistry, itemComponentRegistry. See Custom commands.
    system shutdown Yes Fires before the plugin is unloaded or Basalt stops
    system scriptEventReceive Yes See Script events
    system packetReceive, packetSend (Basalt) Yes packetId, packetSize, and sender or recipients

    playerMove fires very often. Subscribe to it only when you need it, and keep the handler tiny.

    A few events and fields work in Basalt but are not declared in the typings yet, so TypeScript reports an error where you use them:

    • entityHitBlock, entityHealthChanged and itemComponentUse on world.afterEvents,
    • worldLoad on world.beforeEvents,
    • itemComponentRegistry on the startup event.

    The event classes themselves, such as EntityHealthChangedAfterEvent, are declared. Until the typings catch up, reach the signal through a cast:

    import { world, EntityHealthChangedAfterEvent, EventSignal } from "@minecraft/server";
    const afterEvents = world.afterEvents as typeof world.afterEvents & {
    entityHealthChanged: EventSignal<EntityHealthChangedAfterEvent>;
    };
    afterEvents.entityHealthChanged.subscribe(({ entity, oldValue, newValue }) => {
    if (newValue < oldValue && newValue <= 4) console.log(`${entity.typeId} is low on health`);
    });

    Plain JavaScript needs no cast.

    The exact type of every event is in the typings: see @minecraft/server.