Context of the August 2021 Q&A
In mid-2021, Bedrock creators were facing the impending deprecation of the legacy scripting system (which relied on system.initialize and custom event listeners) and the introduction of the GameTest Framework. The August 6 Q&A clarified that GameTest was not just forMojang's internal unit testing of vanilla mechanics, but was actively being expanded to serve as the foundation for all server-side add-on logic. The developers explained that splitting the API into distinct ES6 modules was intentional, allowing general gameplay manipulation to exist separately from structural test assertions.
During this period, scripts were transitionally packaged under the modules array in the behavior pack manifest using the javascript type or early gametest module types, before eventually settling on the standard script type we use today. A major topic of the Q&A was the boundary between the two core modules introduced at the time: mojang-minecraft for world, entity, and block manipulation, and mojang-gametest for spawning test structures, controlling simulated players, and asserting test conditions.
If you are maintaining or studying an add-on built right after the August 2021 discussions, you will encounter these mojang-prefixed module specifiers. In later updates, Mojang moved all scripting packages under the @minecraft namespace to align with standard NPM package conventions. Understanding this lineage is essential when updating older community packs or reading legacy code snippets.
| 2021 Q&A Era Identifier | Modern Equivalent | Status and Migration Notes |
|---|---|---|
| mojang-minecraft | @minecraft/server | Stable. Contains core World, Dimension, Entity, and Block classes. Event subscription syntax has changed significantly. |
| mojang-gametest | @minecraft/server-gametest | Experimental (Beta). Still requires the Beta APIs toggle. Used for Test class, registration, and SimulatedPlayer. |
| mojang-minecraft-ui | @minecraft/server-ui | Stable. Discussed in 2021 as a future goal for form-based UI; later released to handle ActionFormData, ModalFormData, and MessageFormData. |
| World.events.tick | system.runInterval | Removed. Tick events were replaced by the system scheduling methods to improve performance and error isolation. |
| Commands.run() | Dimension.runCommandAsync() | Removed. Global command execution was moved to specific Dimension and Entity instances for clearer execution context. |
Manifest Configuration: Then vs. Now
One of the most frequent questions in the August 2021 session revolved around how to properly wire up a script file inside the behavior pack manifest.json. At the time, the dependency chain was strict and undocumented, requiring creators to guess UUIDs or module names. The developers clarified that external script modules needed to be declared in the dependencies block using module_name rather than a pack UUID, which was a brand-new concept for Bedrock manifests.
Below is a modern, valid manifest configuration that reflects how the architecture discussed in 2021 matured. Instead of the deprecated mojang-minecraft identifier and early 0.1.0 version strings, modern packs use the script module type, specify an entry file inside the scripts/ directory, and declare dependencies on @minecraft/server and @minecraft/server-gametest.
Note that while @minecraft/server has many stable versions (such as 1.12.0 and above), @minecraft/server-gametest remains in beta. Any pack utilizing actual GameTest registration or SimulatedPlayer mechanics must still append the -beta suffix to the version string and enable the Beta APIs experimental toggle on the world.
{
"format_version": 2,
"header": {
"name": "GameTest Architecture Reference Pack",
"description": "Demonstrates modern Script and GameTest module declarations.",
"uuid": "a1b2c3d4-e5f6-4a5b-8c9d-0e1f2a3b4c5d",
"version": [1, 0, 0],
"min_engine_version": [1, 20, 80]
},
"modules": [
{
"description": "Server Script Module",
"type": "script",
"language": "javascript",
"uuid": "b2c3d4e5-f6a7-4b5c-9d0e-1f2a3b4c5d6e",
"version": [1, 0, 0],
"entry": "scripts/main.js"
}
],
"dependencies": [
{
"module_name": "@minecraft/server",
"version": "1.11.0"
},
{
"module_name": "@minecraft/server-gametest",
"version": "1.0.0-beta"
}
]
}Core GameTest Mechanics Discussed
A core focus of the Q&A was explaining how a GameTest actually executes in the world. Unlike general scripts that run globally from the moment a world loads, a GameTest is localized in both space and time. When triggered via the /gametest run command, the engine places a pre-saved .mcstructure file into the world, creates a bounding box around it, and instantiates a Test helper object scoped specifically to that structure.
Community members asked why methods on the Test object took relative coordinates while the general World API used absolute coordinates. The engineers explained that relative coordinates allow tests to be run anywhere in the world—or even in parallel grids of dozens of tests at once—without hardcoding world positions. If a test checks whether a redstone lamp turns on at relative coordinate (2, 1, 2), that same code works regardless of where the test structure is placed in the Overworld.
The Q&A also highlighted the SimulatedPlayer class, which was introduced to test mechanics that only respond to real player presence, such as ironゴーレム spawning, mob despawn spheres, or UI interactions. Simulated players only exist within the @minecraft/server-gametest module. While creators immediately asked if they could use SimulatedPlayer to create NPCs or bots in normal survival gameplay, the team noted that the class was tightly coupled to the GameTest lifecycle, though creators have since found ways to keep tests running indefinitely to use simulated players as chunk loaders or farm helpers.
import * as server from "@minecraft/server";
import * as gametest from "@minecraft/server-gametest";
// Registering a test under the "craftnotes" namespace, named "hopper_transfer"
gametest.register("craftnotes", "hopper_transfer", (test) => {
const spawnPos = { x: 1, y: 2, z: 1 };
const containerPos = { x: 1, y: 1, z: 1 };
// Spawn an item above a hopper inside the test structure
const diamondItem = new server.ItemStack("minecraft:diamond", 1);
test.spawnItem(diamondItem, spawnPos);
// Wait 20 ticks (1 second), then assert the item reached the container below
test.runAfterDelay(20, () => {
test.assertContainerContains(diamondItem, containerPos);
test.succeed();
});
})
.structureName("craftnotes:hopper_test")
.maxTicks(40);
Component Access and Data-Driven Limitations
Another major theme of the August 2021 Q&A was the relationship between JSON entity components and the JavaScript API. Creators coming from the legacy scripting system wanted to know if they could freely read and modify any JSON component on an entity at runtime, such as changing an entity's scale, collision box, or attack damage directly through JavaScript properties.
The development team explained that due to Bedrock's internal C++ architecture, exposing every JSON component directly to JavaScript with full read-write access was not feasible for performance and stability reasons. Instead, each component had to be manually wrapped and exposed as an API class (for example, EntityHealthComponent or EntityInventoryComponent). If a component did not have a dedicated JavaScript wrapper, scripts could not read or mutate its internal values directly.
As a workaround—which the developers themselves recommended during the Q&A and which remains the standard pattern today—creators should use entity events and component groups. When a script needs to alter an entity's behavior or physical traits that aren't directly exposed in the API, the script calls triggerEvent() on the entity. This fires an event defined in the entity's behavior JSON, which in turn adds or removes component groups. In later updates, Mojang also added dynamic properties and improved state syncing via entity properties (formerly permutations), reducing the reliance on component groups for simple data storage.
import { world, system } from "@minecraft/server";
// Modern equivalent of the 2021 tick loop
system.runInterval(() => {
const overworld = world.getDimension("overworld");
const zombies = overworld.getEntities({ type: "minecraft:zombie" });
for (const zombie of zombies) {
// Reading an exposed component (Health)
const healthComp = zombie.getComponent("minecraft:health");
if (healthComp && healthComp.currentValue < 5) {
// Triggering a JSON event to modify unexposed components
zombie.triggerEvent("craftnotes:enter_enraged_state");
}
}
}, 20);
Performance, Watchdogs, and Execution Limits
During the Q&A, creators asked about the performance limits of the new QuickJS engine that powered the GameTest framework, specifically inquiring about file I/O, network requests, and heavy computation. The developers confirmed that the scripting environment is strictly sandboxed: standard Node.js modules, arbitrary file system access, and unsolicited HTTP requests are disabled on standard worlds to prevent malicious packs from harming player devices or hogging server threads.
They also discussed the script watchdog timer. Because Bedrock scripts run synchronously on the main server thread between world ticks, an infinite while loop or a massive nested iteration over thousands of blocks will freeze the entire game. To protect the server, the engine monitors how many milliseconds a script takes to execute during a single tick and tracks total memory allocation. If a script exceeds the execution threshold, the watchdog terminates the script module and logs a spike warning or fatal error.
Since that 2021 discussion, Mojang introduced system.runJob(), which takes a JavaScript generator function and spreads its execution across multiple ticks automatically. Whenever you are porting heavy block-scanning or structure-generation code from early GameTest experiments, wrapping the loop in a generator with yield statements is the proper way to obey the watchdog limits discussed by the engineering team.
Which versions this applies to
Originally discussed around Bedrock Edition 1.17.10–1.17.30 under the 'Enable GameTest Framework' toggle. In modern Bedrock (1.20.x and 1.21.x), general scripting via @minecraft/server is stable and requires no toggles, while @minecraft/server-gametest still requires the 'Beta APIs' experimental toggle.
When it does not work
| What you see | Why | The fix |
|---|---|---|
| Content Log Error: Module [mojang-minecraft] does not exist. | You are attempting to load a script or manifest written during the 2021 Q&A era. The 'mojang-' module prefix was completely removed from the engine in favor of the '@minecraft/' NPM-style namespace. | Update both your BP/manifest.json dependencies and the import statements at the top of your JavaScript files from 'mojang-minecraft' to '@minecraft/server', and 'mojang-gametest' to '@minecraft/server-gametest'. |
| ReferenceError: 'World' is not defined or TypeError: cannot read property 'tick' of undefined (world.events.tick.subscribe) | In 2021, events were accessed through a capitalized 'World.events' object, and later 'world.events'. Modern Script API splits events into 'world.beforeEvents' and 'world.afterEvents', and moved the tick event out of the world object entirely. | Import 'system' from '@minecraft/server' and replace 'world.events.tick.subscribe(...)' with 'system.runInterval(() => { ... }, 1)'. |
| GameTest error: Unable to load structure 'namespace:structure_name' | The GameTest registration expects a .mcstructure file inside the behavior pack, either matching the test name by default or specified via .structureName(), and cannot find it in the expected folder. | Export your structure using a Structure Block and place the .mcstructure file inside 'BP/structures/namespace/structure_name.mcstructure'. Ensure the subfolder inside 'BP/structures/' matches the namespace prefix used in .structureName(). |
| Watchdog Error: Hang detected in script module / Script execution terminated due to timeout. | A synchronous loop (such as iterating over a large 3D volume of blocks) took too many milliseconds in a single tick, triggering the engine's anti-freeze watchdog discussed in the Q&A. | Convert your synchronous loop into a generator function (function* myTask()) that yields periodically, and execute it using 'system.runJob(myTask())' so the engine can spread the work across multiple ticks. |
Reference, straight from the game files
Read out of the game itself, so the ids match the version you are building for.
