Dimensions are separate worlds
This page explains where your code has to run to see players and blocks, who is allowed to write state that all dimensions share, and how a player gets from one dimension to another.
In Paper, a World is an object that your plugin can reach from anywhere. In mcrs, a dimension is a separate simulation, and code in the main app cannot touch it directly. The examples come from repository files, and we did not run them outside the repository tests.
One main app, one sub-app per dimension
Section titled “One main app, one sub-app per dimension”The server has one main Bevy App. Each dimension runs as a SubApp, which is a Bevy app nested in the main one, and it has its own World with its own entities and resources.1 In one shared world, every system would visit the entities of all dimensions and filter them by a dimension component. The dimensions also could not tick in parallel or move to another machine later.
This has consequences for your code. A system of a dimension world sees only the entities of that dimension. Immutable registries are shared with the dimensions as Arc values, and mutable state is not shared.
A dimension world also has no Time resource and does not run the stock Main schedule. It runs First, PreUpdate, the Fixed* schedules, Update, PostUpdate and Last once per host tick. A dimension therefore counts ticks and cannot depend on the wall clock, as the doc comment in sub_app.rs says.
A plugin gets into a dimension through add_dimension_plugin
Section titled “A plugin gets into a dimension through add_dimension_plugin”A plugin added with add_plugins goes to the main app only, where it sees no players and no blocks. To put a plugin into the dimensions, use the AddDimensionPlugins trait:2
pub trait AddDimensionPlugins { fn add_dimension_plugin(&mut self, plugin: impl Plugin + Clone) -> &mut Self;
fn add_dimension_plugin_to( &mut self, dimension: impl Into<ResourceKeyBuf<Dimension>>, plugin: impl Plugin + Clone, ) -> &mut Self;}crates/mcrs_minecraft_engine_world/src/world/sub_app.rs:112-120
add_dimension_plugin(plugin)builds a copy of the plugin in every dimension world.add_dimension_plugin_to(key, plugin)builds it only in the dimension with that key, for exampleOVERWORLD.
The plugin must be Clone, because each world builds its own clone. The worlds build the clones in the order the plugins were added. Add your plugin before the dimensions spawn, because a dimension that is already live does not get a later plugin.
One more rule applies to unique plugins, which is the Bevy name for a plugin that an app can hold only once. A unique plugin that you add for one dimension replaces, in that world, the unique plugins with the same name that were added for all dimensions. Any other unique plugin whose name the world already has is skipped, and the server logs an error.
The repository test checks each rule. It adds one counter to every world and another counter only to the overworld. Then it asserts that the nether has no second counter and that the overworld got the value of the plugin added for its key.
#[test]fn dimension_plugins_run_in_every_world_or_in_the_world_of_their_key() { let mut app = host_app::make_host_app(); app.add_dimension_plugin(CountTicks::<true>) .add_dimension_plugin(Rate(1)) .add_dimension_plugin_to(OVERWORLD, SeesSpawn) .add_dimension_plugin_to(OVERWORLD, AddsCountTicks) .add_dimension_plugin_to(OVERWORLD, CountTicks::<false>) .add_dimension_plugin_to(OVERWORLD, Rate(2)); host_app::materialise_sub_apps( &mut app, &[ ("minecraft:overworld", "minecraft:overworld"), ("minecraft:the_nether", "minecraft:the_nether"), ], ); for _ in 0..5 { app.update(); }
assert!(ticks::<true>(&mut app, "minecraft:overworld").is_some_and(|n| n > 0)); assert!(ticks::<true>(&mut app, "minecraft:the_nether").is_some_and(|n| n > 0)); assert!(ticks::<false>(&mut app, "minecraft:overworld").is_some_and(|n| n > 0)); assert_eq!(ticks::<false>(&mut app, "minecraft:the_nether"), None); assert_eq!( world(&mut app, "minecraft:overworld") .resource::<RateSet>() .0, 2 ); assert_eq!( world(&mut app, "minecraft:the_nether") .resource::<RateSet>() .0, 1 );crates/mcrs_minecraft_server/tests/it/dimension_plugins.rs:90-125
Global state has one writer
Section titled “Global state has one writer”Every dimension reads game time, world clocks, weather, game rules, the world border and scoreboards. Each piece of such state has one owner, and only the owner writes it.3 In one process the owner is the main app.
A dimension holds a read-only snapshot with a version, and it gets a new snapshot only when the version changes. When a dimension wants a change, for example because of a command or a weather cycle, it sends a message to the owner.
For a plugin this means that you do not keep your own copy of the game clock inside a dimension and advance it, because the copies would drift apart. The same holds for a test setup. When it sets game rules, weather or clocks, it writes through the owner and never into a dimension world.
A player moves between dimensions as a message
Section titled “A player moves between dimensions as a message”A system of one dimension cannot reach another world, so the server cannot move the entity of a player from one world to the other. A portal, or any other move decided inside a tick, writes a request:1
#[derive(Message, Clone, Debug)]pub struct ChangeDimensionRequest { pub player: Entity, pub to: ResourceKeyBuf<Dimension>, pub transform: Transform,}crates/mcrs_minecraft_engine_world/src/entity/player/mod.rs:19-24
At the end of the tick, the old world takes the carried state of the player, despawns the player and hands a departure to the host. The player arrives in the new world on the next tick. The rules are in the section “Change of dimension” of docs/command-dimensions.md.
The same document describes how commands work across worlds. A command runs in one world. Its effects on other worlds are messages, and the host forwards them between ticks. The page Add a command shows how a handler writes such an effect.
You cannot create a dimension at run time yet
Section titled “You cannot create a dimension at run time yet”Next: Remove and replace vanilla parts.
Footnotes
Section titled “Footnotes”-
Partial#362 The decision that each dimension runs as a sub-app with its own world. ↩ ↩2
-
Works#280 Lets user plugins run inside every dimension world. ↩ ↩2
-
Works#357 The decision that global mutable state has one writer. ↩
-
WIP#546 Plans to create and remove dimension worlds at run time and move players between them. ↩
-
WIP#281 Plans the chunk source as a replaceable plugin point, with empty and flat sources. ↩
-
Partial#283 Tracks public player events that a plugin can cancel. ↩
-
WIP#142 Tracks that players arrive in the target dimension as full players. ↩
Sources and verification
Verified: 26.4-snapshot-7, 51d45c26a, 2026-11-09