What behaves differently from Bedrock Dedicated Server's scripting, what is missing and what Basalt adds.
Basalt’s Plugin API is a port of Mojang’s scripting API: @minecraft/server 2.x, @minecraft/server-ui 2.x and @minecraft/server-net. Names and argument shapes are Mojang’s, so most scripts written for BDS run unchanged. This page covers the places where they do not, so you know what to check when porting a pack or writing one that must run on both.
Loading and packaging
Section titled “Loading and packaging”| BDS | Basalt | |
|---|---|---|
| Where scripts live | A behaviour pack attached to the world | A folder in plugins/ with a manifest.json |
| TypeScript | Compile to JavaScript first | .ts files run directly; types are stripped, not checked |
@minecraft/server-net |
Needs a per-pack permissions file | Available to every plugin unless script_net_enabled is off |
| Module versions | The manifest picks the API version | One API is provided; the version in dependencies is not used to select anything |
Other loading rules:
- A plugin can import only its own files and the four provided modules. There is no npm, and no importing another plugin’s files.
- A dependency on a
@minecraft/module Basalt does not provide, such as@minecraft/server-gametestor@minecraft/server-admin, logs a warning, and importing it fails. - Each plugin runs in its own script engine on its own thread, so plugins share no globals.
/reloaddrops every subscription, timer and custom command and starts each plugin fresh.- Dynamic properties are stored per plugin, keyed by
header.uuid.
Before events and reads
Section titled “Before events and reads”This is the biggest behavioural difference.
On BDS, before events run in a restricted mode where the world can be read but not changed. On Basalt it is the other way round:
- Changes are allowed inside before events and command callbacks.
teleport,applyImpulse,sendMessageand the like are sent as the handler returns. - Reads are restricted. In block-break, block-place and mob-hurt before events, reading the world throws. In player before events and command callbacks, a player is read as the plugin last saw them, and a first read throws.
Code that follows BDS’s rule works on both: read nothing you do not need in the handler, and wrap changes in system.run. Details and examples are in Events.
Other differences in timing:
- A before handler has 50 ms. Handlers that take more than 10 ms are reported in the log and in
/status. - Reads outside before events ask the game and wait for the answer, for up to a second. Several reads of the same player or entity in one handler are answered from one snapshot until the handler changes something.
world.afterEvents.worldLoadfires on the first tick after startup, once the top-level code of every module has run. Basalt also fires aworld.beforeEvents.worldLoadjust before it. The old nameworldInitializeworks on both and logs a deprecation warning.
One dimension
Section titled “One dimension”Basalt has only the overworld.
world.getDimension("overworld")works. Any other id throws.Entity.dimensionis always the overworld.- A
dimensionpassed in teleport options is accepted and ignored. - There is no
playerDimensionChangeevent. MinecraftDimensionTypes, the dimension ids as the older API exported them, is there for old packs. Only the overworld id gives a dimension.
Commands
Section titled “Commands”player.runCommandruns the command as the player, with the player’s own permission level. A member cannot run operator commands this way. Usedimension.runCommand, which runs with full rights, or use the API directly.dimension.runCommandthrows when the command fails.- Only Basalt’s own commands exist. A script that runs
/execute,/fill,/setblock,/tellrawor another command Basalt does not have will fail there. See Commands. - In custom commands,
EntitySelectorparameters resolve to players only. CommandResult.successCountis 1 for a success rather than a count of targets.
RawMessage objects are accepted wherever BDS accepts them, but a translate key is sent as the key itself rather than being translated. Prefer plain text.
Redstone events
Section titled “Redstone events”buttonPush, leverAction, pressurePlatePush, pressurePlatePop, targetBlockHit, tripWireTrip and pistonActivate fire, but Basalt does not track who caused them: source, player and sources are empty. pistonActivate gives the piston’s state as plain values, and getAttachedBlocks is not available.
Smaller behavioural differences
Section titled “Smaller behavioural differences”Player.getVelocity()is the motion the client last reported.playSoundreturns nothing; there is no sound instance to control.Container.addItemdoes not report what did not fit.Dimension.fillBlocksreturns the number of blocks changed.Dimension.spawnItemreturns Basalt’sItemEntity.Dimension.getEntitiesincludes players.Dimension.getEntitiesFromRayandEntity.getEntitiesFromViewDirectiontreat each entity as a sphere around its middle, not its real hitbox, so a hit at the very edge can differ.Dimension.getBlocksandcontainsBlocksearch at most 262,144 blocks. A larger volume, or one that reaches unloaded chunks, throws anUnloadedChunksError, or gives no blocks withallowUnloadedChunks. The filter’sincludePermutationsandexcludePermutationsare not read.Entity.applyDamageignores its options: the damage has no cause or attacker.Block.getItemStackgives the item with the block’s own id, so the few blocks whose item has a different id are not mapped.Entity.isSleeping,world.isHardcoreandsystem.isEditorWorldare always false.entityHitBlockdoes not sendhitBlockPermutation.entityHealthChangedfires for mobs only when they take damage.- Custom item components registered through
itemComponentRegistryonly haveonUsecalled, with no parameters argument. inputInfo.touchOnlyAffectsHotbaris always false.isSneaking,isSprinting,isSwimmingandisClimbingare only meaningful for players.- In
playerPlaceBlock,blockis the block being placed at its position. - Container menus from
@basalt/menusshow workstation windows, but workstations in a menu do not craft.
Not implemented
Section titled “Not implemented”The typings in plugins/node_modules follow Mojang’s API closely so that existing code type-checks. They follow @minecraft/server 2.11.0 and declare several hundred classes, interfaces and members that Basalt does not implement. Using something that is not implemented fails at runtime: a missing method throws a TypeError when called, and a missing property reads as undefined.
A few things are also missing from the typings although they work. See Events the typings do not list yet.
Basalt is developed quickly and this list shrinks with each version. At the time of writing, the following are not implemented:
| Area | Missing |
|---|---|
| Players | camera, fogSettings, locatorBar, graphicsMode, commandPermissionLevel, playerPermissionLevel, postClientMessage, aim assist, control schemes, the chat name parts, per-entity property overrides |
| Entities | playAnimation, tryTeleport, getAABB, getAllBlocksStandingOn, nameplateDepthTested, nameplateRenderDistance; most entity components beyond health, inventory, equippable, rideable, riding and item |
| Blocks | Block components (getComponent), and so block inventories; light levels, setWaterlogged and the other liquid queries; getParts |
| Dimension | cloneBlocks, light levels, biome queries, placeFeature, placeFeatureRule |
| World | setAbsoluteTime, the loot table manager, world clocks, pack settings, primitive shapes, the sound definition registry, aim assist |
| Containers | weight, containerRules |
| Structures | structureManager.createEmpty, and reading or writing blocks inside a Structure |
| Scoreboard | Teams; ScoreboardIdentity.getEntity() for entities |
| Events | entityHeal, playerButtonInput, playerInventoryItemChange, gameRuleChange, playerDimensionChange, dataDrivenEntityTrigger, container open and close events, and any other event the Event reference does not list |
| Custom content | Custom block components registered from scripts; for custom item components, every hook but onUse |
| Modules | @minecraft/server-gametest, @minecraft/server-admin, @minecraft/server-editor, @minecraft/debug-utilities |
If you depend on something less common, test it on Basalt before building on it. The reliable core is what the guides in this section use: the events listed in Events, players and their inventories, items, blocks, entities, scoreboards, dynamic properties, forms, commands and HTTP.
Basalt additions
Section titled “Basalt additions”These exist on Basalt and not on BDS. A plugin that uses them will not run on BDS.
| Addition | Where |
|---|---|
The server object: beforeJoin, playerCommand, playerDie and entityDie before events, and server.plugin |
@minecraft/server |
Extra world events: playerHurt, playerMove, playerTeleport, playerRespawn, playerPickupItem, playerDropItem, playerToggleSneak, playerToggleSprint, playerChangeSkin, playerViolation, playerFormResponded, itemSpawn, itemComponentUse, and before events for playerEmote and worldLoad |
@minecraft/server |
knockback and cancelKnockback on hurt events |
@minecraft/server |
Player extras: kick, sendToast, addBossBar, clearEffects, locale, hidePlayer, showPlayer, canSee, setVelocity |
@minecraft/server |
ItemEntity, with owner, lifetime, pickup delay and per-player visibility |
@minecraft/server |
Dynamic properties on blocks: Block.getDynamicProperty, setDynamicProperty and the rest |
@minecraft/server |
Chunk control on Dimension: getChunk, loadChunk, unloadChunk |
@minecraft/server |
world.loadStructure and world.hasStructure |
@minecraft/server |
system.afterEvents.packetReceive and packetSend |
@minecraft/server |
CustomForm, uiManager.closeScreen, uiManager.setServerSettings |
@minecraft/server-ui |
WebSocket and HttpError |
@minecraft/server-net |
| Container menus | @basalt/menus |
Basalt’s additions are marked “Basalt’s own” in the typings’ documentation comments.
Writing a plugin that runs on both
Section titled “Writing a plugin that runs on both”-
Stick to Mojang’s API. Avoid everything in the table above, or guard it:
import * as mc from "@minecraft/server";const onBasalt = "server" in mc; -
Inside before events and command callbacks, do not read the world, and wrap changes in
system.run. -
Use
world.afterEvents.worldLoadfor setup. -
Use the 2.x form of
ModalFormDataoptions ({ defaultValue }objects). -
Avoid deprecated names. Basalt accepts
runCommandAsync,world.say,system.runScheduleandworldInitializefor old packs, but logs a warning for each. -
Test on both. Basalt checks nothing at type level when it loads a plugin.