The Core Paradigm Shift: Inheritance vs. Composition
In Java Edition, modding is overwhelmingly object-oriented and relies on inheritance. If you want to create a custom zombie or a block that behaves like a furnace, you extend the vanilla `Zombie` or `AbstractFurnaceBlock` class, override a few methods like `tick()` or `use()`, and register your new object during a mod-loader event. If the vanilla class does not expose a hook you need, you write a Spongepowered Mixin to inject bytecode directly into the vanilla method. You have total access to the client and server internals, at the cost of your mod breaking almost every time Mojang obfuscates or refactors a major update.
Bedrock Edition is written in C++ and does not allow bytecode injection or arbitrary native code execution on retail devices (consoles, mobile, and PC). Instead, Bedrock uses an Entity-Component-System (ECS) architecture exposed through two distinct folders: a Behavior Pack (`BP/`) for server-side logic and data, and a Resource Pack (`RP/`) for client-side visuals and audio. You do not extend a base class in code. To build an entity or block, you author a JSON file and attach discrete, pre-compiled C++ components to it, such as `minecraft:health`, `minecraft:navigation.walk`, or `minecraft:destructible_by_mining`. If a vanilla behaviour is not exposed as a JSON component or a Script API method, you must either build an approximation using scripts or redesign your feature.
Backward compatibility is the primary benefit of this sandbox. Because your JSON files declare a `format_version` at the top, the Bedrock engine knows how to parse an entity or block file written three years ago alongside one written for the latest release. You trade raw, unrestricted engine access for cross-platform compatibility and stability across game updates.
| Java Modding Concept | Bedrock Add-on Equivalent | Architectural Notes |
|---|---|---|
| Mod JAR file | .mcaddon / .mcpack archive | Split into a Behavior Pack (server data/scripts) and Resource Pack (client assets). |
| mods.toml / fabric.mod.json | BP/manifest.json & RP/manifest.json | Requires unique UUIDs for the header and every module. Scripts declare explicit npm-style version dependencies. |
| Subclassing Block / Item / Entity | Data-driven JSON + Components | Behaviours are assembled by combining official `minecraft:*` components in JSON. |
| Forge/Fabric Event Bus | @minecraft/server World Events | Divided into `beforeEvents` (cancellable, read-only world state) and `afterEvents` (not cancellable, allows world mutation). |
| NBT (Named Binary Tag) in code | Dynamic Properties | Primitive data (strings, numbers, booleans, Vector3) stored via `setDynamicProperty` on entities, items, or the world. |
| Network Packets (SimpleChannel / Payload) | None (Handled by engine or workarounds) | Scripts run exclusively on the server. There is no client-side Script API to receive custom network packets. |
| Mixins / Coremods / ASM | Not supported | You cannot alter closed C++ engine code. You can only override vanilla JSON files or intercept Script API events. |
Blocks and Items: JSON Components + Custom Components
In a Java mod, a custom block is a Java class instantiated and registered in a `DeferredRegister`. Its properties (hardness, resistance, light emission) are passed into a `BlockBehaviour.Properties` builder, while its interactive behaviour lives in overridden methods like `useWithoutItem` or `stepOn`. Block states are defined via a `StateDefinition` builder, and complex persistent data requires attaching a `BlockEntity` (formerly TileEntity) class.
In Bedrock, physical properties and block states are declared purely in a Behavior Pack JSON file under `BP/blocks/`. Block states are defined under the `description.states` object (supporting boolean, integer, and string arrays up to a strict limit of permutations), and conditional changes to the block's geometry, collision, or material based on those states are handled using the `permutations` array and Molang queries. Instead of writing a `BlockEntity` for simple data, you rely on block states; if you need arbitrary data storage or complex ticking per block, you typically attach a hidden marker entity or store coordinates in world dynamic properties, as Bedrock does not expose custom C++ BlockEntities to add-ons.
For interactive logic—like what happens when a player right-clicks a block, steps on it, or when it receives a random tick—Bedrock uses the V2 Custom Components system. In your block or item JSON, you add the `minecraft:custom_components` component and list string identifiers for your behaviours. Then, during the `worldInitialize` (or `startup` in newer API builds) event in your TypeScript/JavaScript code, you register an object containing your callback functions (`onPlayerInteract`, `onTick`, `onStepOn`, `onPlayerDestroy`) against that exact string identifier.
{
"format_version": "1.21.10",
"minecraft:block": {
"description": {
"identifier": "example:charged_copper",
"states": {
"example:is_active": [false, true]
}
},
"components": {
"minecraft:destructible_by_mining": {
"seconds_to_destroy": 3.0
},
"minecraft:destructible_by_explosion": {
"explosion_resistance": 6.0
},
"minecraft:geometry": "minecraft:geometry.full_block",
"minecraft:material_instances": {
"*": {
"texture": "charged_copper_off",
"render_method": "opaque"
}
},
"minecraft:tick": {
"interval_range": [20, 20],
"looping": true
},
"minecraft:custom_components": [
"example:charge_logic"
]
},
"permutations": [
{
"condition": "q.block_state('example:is_active') == true",
"components": {
"minecraft:light_emission": 12,
"minecraft:material_instances": {
"*": {
"texture": "charged_copper_on",
"render_method": "opaque"
}
}
}
}
]
}
}Server-Side Scripting and Read-Only Execution Privileges
When you write a Java mod, your code runs inside the same thread as the server (or client) tick loop. If an event fires—such as `LivingHurtEvent` or `PlayerInteractEvent`—you can immediately cancel the event, spawn three entities, set a block in the world, and modify the player's inventory in the exact same stack frame. The JVM trusts you not to corrupt the world state while it is mid-calculation.
Bedrock's `@minecraft/server` API runs inside a QuickJS JavaScript engine alongside the C++ server thread. It separates world events into two strict categories: `beforeEvents` and `afterEvents`. A `beforeEvent` fires before the C++ engine commits the action; this allows you to set `event.cancel = true` to stop a block from breaking or a chat message from sending. However, during a `beforeEvent` callback (and similarly during custom component callbacks in earlier API versions, depending on the exact hook), the JavaScript environment is placed in a **Read-Only mode**. If you try to call `dimension.setBlockType()`, `entity.kill()`, or `inventory.Container.addItem()` inside a `beforeEvent`, the engine throws a native privilege error.
To mutate the world in response to a `beforeEvent`, you must wrap your world-modifying code inside `system.run(() => { ... })`. This schedules the callback to run at the end of the current tick, once the C++ engine has exited its sensitive state calculation and granted full read/write privileges back to the script environment. On the other hand, `afterEvents` and V2 custom component hooks (in current 1.21+ releases) already execute with read/write privileges, though you cannot cancel the physical action that triggered them because it has already occurred.
import { world, system, BlockPermutation } from "@minecraft/server";
// Registering the custom component referenced in BP/blocks/charged_copper.json
world.beforeEvents.worldInitialize.subscribe((initEvent) => {
initEvent.blockComponentRegistry.registerCustomComponent("example:charge_logic", {
onPlayerInteract(event) {
const { block, dimension } = event;
const currentState = block.permutation.getState("example:is_active");
// Flip the custom boolean state
block.setPermutation(
block.permutation.withState("example:is_active", !currentState)
);
dimension.playSound("random.click", block.location);
},
onTick(event) {
const { block, dimension } = event;
if (block.permutation.getState("example:is_active")) {
dimension.spawnParticle("minecraft:villager_happy", {
x: block.location.x + 0.5,
y: block.location.y + 1.1,
z: block.location.z + 0.5
});
}
}
});
});
// Intercepting a vanilla action using beforeEvents + system.run for world mutation
world.beforeEvents.playerBreakBlock.subscribe((event) => {
if (event.block.typeId === "minecraft:diamond_ore") {
// 1. We can cancel the event directly in beforeEvents
event.cancel = true;
// 2. We MUST defer world mutations to the next available read-write window
system.run(() => {
event.player.sendMessage("Diamond ore is protected here!");
event.dimension.spawnEntity("minecraft:silverfish", event.block.location);
});
}
});Entities, AI Goals, and Molang vs. Java GoalSelector
In Java, custom entity AI is built by registering `Goal` subclasses into an entity's `goalSelector` and `targetSelector`. Because you write these goals in Java, you can check any condition you want in `canUse()` and execute any movement or attack code in `tick()`. Animations and rendering are handled on the client using Java classes like `EntityRenderer` and `EntityModel` (or libraries like GeckoLib, which ironically originated to bring Bedrock-style animation workflows back to Java).
In Bedrock, the entity pipeline is natively data-driven and significantly more modular, though closed to custom C++ goals. In the Behavior Pack (`BP/entities/`), you assemble AI from vanilla components like `minecraft:behavior.melee_attack` or `minecraft:behavior.random_stroll`. To make an entity change its behaviour dynamically—for example, switching from passive to hostile, or changing phases in a boss fight—you use **Component Groups** and **Entity Events**. An event adds or removes component groups on the fly. For custom AI logic that cannot be expressed by combining vanilla `minecraft:behavior.*` components, you cannot write a new AI goal class; instead, you run a script interval in `@minecraft/server` and control the entity via methods like `applyKnockback()`, `teleport()`, or by triggering entity events with `entity.triggerEvent()`.
On the Resource Pack side (`RP/`), Bedrock uses a dedicated scripting language called **Molang** (Mojang Language). Where a Java developer writes math inside `setupAnim()` in a model class to rotate a leg bone based on limb swing, a Bedrock developer writes Molang expressions directly inside `RP/animations/` JSON files and `RP/render_controllers/`. Molang evaluates fast on the client C++ thread, reading entity state via queries like `q.modified_move_speed`, `q.is_baby`, or custom synced data stored in **Entity Properties** (declared in the BP entity description and read on the client via `q.property('example:phase')`).
User Interfaces: Screen Classes vs. @minecraft/server-ui and JSON UI
Creating a custom GUI in Java typically splits into two categories: client-only screens (extending `Screen` and overriding `render` to draw textures and widgets) and inventory containers (combining a server-side `AbstractContainerMenu` with a client-side `AbstractContainerScreen` to sync item slots). You have pixel-perfect control over where slots sit, how progress bars render, and what happens when a button is clicked.
Bedrock splits UI into two very different systems, and neither works directly like a Java `ContainerMenu`. The easiest and most stable method for non-inventory menus is the `@minecraft/server-ui` module. It provides three script-driven classes: `ActionFormData` (a list of buttons with optional icons), `ModalFormData` (dropdowns, sliders, toggles, and text inputs), and `MessageFormData` (a simple two-button yes/no dialog). You construct these in your server script, call `.show(player)`, and await a JavaScript Promise that resolves when the player submits or closes the form.
However, Bedrock **does not natively support custom block inventory screens** through a simple API. You cannot define a custom 3-slot furnace UI with a custom progress arrow in two files like you can in Java. If an add-on needs an inventory GUI, developers usually attach a hidden container entity (like an invisible donkey, chest boat, or NPC/villager variant) to the custom block, name the entity dynamically, and intercept its UI. To change how that screen actually looks on the client, creators have to modify **JSON UI** (`RP/ui/`), which is Bedrock's internal, minimally documented UI layout language. JSON UI uses data-bindings to hijack vanilla screens based on the container's title string, hiding vanilla elements and showing custom textures. It is powerful, fragile across game updates, and frequently breaks when multiple resource packs edit the same vanilla screen file.
World Generation and Dimensions: What Does Not Map Across
Before starting a large port from Java to Bedrock, you need to know where the hard engine boundaries sit. While Java data packs and mods can register entirely new dimensions, custom biome grids, custom noise routers, and complex jigsaw structures, Bedrock's world generation pipeline is much more restricted.
First, **you cannot create custom dimensions** in Bedrock add-ons. You are limited to `minecraft:overworld`, `minecraft:nether`, and `minecraft:the_end`. Any Java mod centered around portal travel to a new dimension must be redesigned in Bedrock—usually by generating structures high in the sky, deep underground, or far out in the End void, and teleporting the player there while using custom fog (`RP/fogs/`) and skyboxes to fake the environment.
Second, **world generation features and biomes have distinct limitations**. Bedrock supports data-driven `BP/features/` and `BP/feature_rules/` (allowing you to place custom ores, trees, vegetation, and scatter single `.mcstructure` files across biomes without experimental toggles). However, adding entirely custom biomes or drastically altering terrain heightmaps and noise generation has historically been locked behind experimental toggles or restricted to modifying existing vanilla biome client files. Similarly, Bedrock lacks a custom shader pipeline for retail add-ons; since the engine moved to RenderDragon, third-party GLSL shaders do not load, and visual customisation is restricted to particles, fog definitions, and Deferred Technical Preview PBR textures (where enabled).
Which versions this applies to
Applies to Minecraft Bedrock Edition 1.21.0 and newer. Standard JSON blocks, items, entities, features, and the stable @minecraft/server and @minecraft/server-ui modules do not require any experimental toggles. Beta API features (such as certain unreleased methods or events) require the 'Beta APIs' world toggle and a '-beta' version string in manifest.json.
When it does not work
| What you see | Why | The fix |
|---|---|---|
| ReferenceError: Native function [Dimension::spawnEntity] does not have required privileges. | You attempted to modify the world state (spawning an entity, setting a block, modifying an inventory, or changing an entity component) directly inside a `beforeEvents` callback, when the C++ engine is locked in read-only mode. | Import `system` from `@minecraft/server` and wrap your world-modifying code inside `system.run(() => { ... })` so it executes on the next available read-write tick window. |
| Custom block or item loads into the world with a black-and-magenta checkered texture and displays as 'tile.example:block_name.name'. | Coming from Java, developers often expect textures in `RP/textures/blocks/my_block.png` to bind by filename automatically. Bedrock requires an intermediate short-name mapping in the Resource Pack and explicit localization entries. | Map a short texture name to the file path inside `RP/textures/terrain_texture.json` (for blocks) or `RP/textures/item_texture.json` (for items), reference that short name in your Behavior Pack component, and add the translation string to `RP/texts/en_US.lang`. |
| Failed to load script module: Dependency version mismatch for '@minecraft/server'. | The `@minecraft/server` version declared under `dependencies` in `BP/manifest.json` either uses a `-beta` suffix without the 'Beta APIs' toggle enabled on the world, or specifies a stable version number higher than what the current game client supports. | Open `BP/manifest.json` and change the `@minecraft/server` dependency version to a stable release supported by your game version (for example, `"1.13.0"` or `"1.14.0"` in 1.21.x), removing `-beta` unless you intentionally enabled the Beta APIs experimental toggle. |
Reference, straight from the game files
Read out of the game itself, so the ids match the version you are building for.
