Skip to content

Remove and replace vanilla parts

Changing. Follow: #278, #279, #281, #282

This page shows how to build your own server from the same plugins as the stock one, how to leave a plugin out, and how to pass settings to the plugins.

In Paper you start from a finished server and add plugins. In mcrs you start from a group of plugins and take parts out, so a lobby can run without world generation and a test can run without lighting. The examples come from repository files, and we did not run them outside the repository tests.

MinecraftServerPlugins is a Bevy PluginGroup, which is a set of plugins that you add to the app as one value. Its four fields are the parts that take settings:

The fields of the group
pub struct MinecraftServerPlugins {
pub core: ServerCorePlugin,
pub worlds: DimensionWorldsPlugin,
pub world_gen: WorldGenPlugin,
pub network: ServerNetworkPlugin,
}

crates/mcrs_minecraft_server/src/lib.rs:22-27

build() adds the rest of the vanilla server. The members, in the order build() adds them, are the server core, world generation registries, the vanilla command set, vanilla commands, world commands, dimension worlds, chat, entity physics, mob brains, world generation, lighting and the network.1

The members of the group
impl PluginGroup for MinecraftServerPlugins {
fn build(self) -> PluginGroupBuilder {
PluginGroupBuilder::start::<Self>()
.add(self.core)
.add(WorldgenRegistriesPlugin)
.add(CommandPlugin::<VanillaCommands>::new())
.add(VanillaCommandsPlugin)
.add(WorldCommandsPlugin)
.add(self.worlds)
.add(VanillaChatPlugin)
.add(VanillaEntityPhysicsPlugin)
.add(VanillaMobBrainPlugin)
.add(self.world_gen)
.add(LightingPlugin)
.add(self.network)
}
}

crates/mcrs_minecraft_server/src/lib.rs:61-77

You can disable or replace each member without editing the server crate. A member that needs another one panics when the group builds and names the missing plugin. You get the error at start, so you do not have to trace a missing resource at the first tick.

The vanilla gameplay lives in plugins above the engine. Each of them is removable on its own, and they reach every dimension world through add_dimension_plugin.2

The binary switches mob AI off like this:

src/main.rs: switch off mob AI
if !settings.mob_ai {
server = server.disable::<VanillaMobBrainPlugin>();
}

src/main.rs:92-94

disable::<T>() is a method of the Bevy plugin group builder, and T is the plugin you leave out. A repository test uses the same method to start a server without world generation and lighting:

Test: a server without world generation and lighting
fn a_client_without_an_app_joins_the_embedded_server_offline() {
let mut plugins = MinecraftServerPlugins::embedded();
plugins.core.asset_path = Some(ASSETS.to_owned());
plugins.network.announce_on_lan = false;
let mut app = App::new();
app.add_plugins(
plugins
.build()
.disable::<WorldGenPlugin>()
.disable::<LightingPlugin>(),
);
let stop = StopOnDrop(Arc::new(AtomicBool::new(false)));

crates/mcrs_minecraft_server/tests/it/client_join.rs:31-42

That test also calls MinecraftServerPlugins::embedded(). It gives a server that runs inside another process. Such a server listens on loopback only, takes a port that the OS assigns, and leaves the task pools to the host app.

embedded()
impl MinecraftServerPlugins {
/// A server running inside another process: loopback only, an OS-assigned
/// port, and the task pools left to the host app.
pub fn embedded() -> Self {
let defaults = Self::default();
Self {
core: ServerCorePlugin {
owns_task_pools: false,
..defaults.core
},
network: ServerNetworkPlugin::embedded(),
..defaults
}
}
}

crates/mcrs_minecraft_server/src/lib.rs:45-59

The binary reads its settings from the environment and turns them into plugin fields in src/main.rs:

src/main.rs: from settings to plugin fields
let settings = ServerSettings::read(
|name| std::env::var(format!("MCRS_{name}")).ok(),
|error| bevy_log::warn!("{error}"),
);
let mut server = MinecraftServerPlugins {
core: ServerCorePlugin {
compute_threads: settings.compute_threads,
..MinecraftServerPlugins::default().core
},
worlds: DimensionWorldsPlugin {
world: world_folder(),
ops,
seed: settings.world_seed,
preset: settings.world_preset.clone(),
default_game_mode: settings.default_game_mode,
..Default::default()
},
world_gen: WorldGenPlugin,
network: network(&settings),
}
.build();

src/main.rs:71-91

Only the binary does this. A library crate does not read an environment variable or a file for its settings, and every setting is a field of the plugin that uses it.3

The environment variables of the binary are the fields of ServerSettings. Each variable has the name of its field with the prefix MCRS_. The full list is in the source file.

In your own program, skip ServerSettings and fill the plugin fields yourself. An embedded server in a test then starts with explicit settings, whatever the shell exported.

Next: Add a command.

  1. Works#279 Splits the server plugin into a group of removable plugins. ↩

  2. Works#282 Moves vanilla gameplay out of the server crate into removable plugins. ↩

  3. Works#278 The server is configured only through its plugin. ↩

  4. WIP#281 Plans the chunk source as a replaceable plugin point, with empty and flat sources. ↩

  5. WIP#78 Tracks saving changed chunks back to region files. ↩

Sources and verification

Verified: 26.4-snapshot-7, 51d45c26a, 2026-11-09

Tracking: #278, #279, #281, #282