Your first plugin: count ticks
In this tutorial you add a plugin that counts server ticks. mcrs has no plugin loader and no jar files, as This is not the Paper API explains, so “installing a plugin” means adding Rust code to the program that starts the server. Your plugin lives in your own copy of the mcrs repository, next to the server binary.
Eight terms this page uses
Section titled “Eight terms this page uses”If Rust and Bevy are new to you, read this table first. Rust for Java developers has the full minimum.
| Word | Closest Java idea | Meaning here |
|---|---|---|
| plugin | JavaPlugin with onEnable |
a struct with a build method that adds systems and resources to an app |
| system | a scheduled task or an event handler | a function that Bevy runs on a schedule, such as FixedUpdate |
| resource | a singleton field of the server | one value per world, read with Res<T> or ResMut<T> |
| observer | an event listener | a function that runs when an event is triggered, written with On<Event> |
| query | a stream over all players, with a filter | a system parameter that visits entities with the components you name |
&mut |
a mutable reference | a borrow that may change the value; only one can exist at a time |
Clone |
clone() |
a copy of the value; a dimension plugin needs it, as step 2 explains |
| dimension world | one World object per dimension |
each dimension has its own world, and your plugin is built once in each |
Step 1: clone the repository
Section titled “Step 1: clone the repository”Clone andreypfau/mcrs. The README Quick start has the toolchain note and the run commands.
Step 2: write the plugin
Section titled “Step 2: write the plugin”A plugin is a struct that implements Plugin. The plugin below registers a resource named Ticks, which holds a number, and one system. The system runs in FixedUpdate and adds one to the number on each server tick.
#[derive(Resource, Default)]struct Ticks<const ALL: bool>(u32);
#[derive(Message)]struct Ticked;
#[derive(Clone)]struct CountTicks<const ALL: bool>;
impl<const ALL: bool> Plugin for CountTicks<ALL> { fn name(&self) -> &str { if ALL { "count every tick" } else { "count overworld ticks" } }
fn build(&self, app: &mut App) { app.init_resource::<Ticks<ALL>>().add_systems( FixedUpdate, |mut ticks: ResMut<Ticks<ALL>>, mut ticked: MessageWriter<Ticked>| { ticks.0 += 1; if ALL { ticked.write(Ticked); } }, ); app.add_message::<Ticked>(); }}crates/mcrs_minecraft_server/tests/it/dimension_plugins.rs:10-40
This code comes from a test file of the repository, where the plugin counts ticks in the overworld and in the nether. The test uses a const generic ALL to run two variants of the plugin. Read the code as a plain CountTicks and ignore ALL.
Your plugin must be Clone, because the server builds one copy of it for every dimension world. The server binary already has a small plugin that shows the shape:
#[derive(Clone)]struct LogInsteadOfPanicPlugin;
impl Plugin for LogInsteadOfPanicPlugin { fn build(&self, app: &mut App) { app.insert_resource(FallbackErrorHandler(log_instead_of_panic)); }}Step 3: register it with add_dimension_plugin
Section titled “Step 3: register it with add_dimension_plugin”The function serve() in src/main.rs already registers one plugin the right way. Add yours on the next line.
fn serve() -> AppExit { let mut app = App::new(); app.insert_resource(FallbackErrorHandler(log_instead_of_panic)); app.add_dimension_plugin(LogInsteadOfPanicPlugin);The test shows both calls and what the server builds from them:
#[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));crates/mcrs_minecraft_server/tests/it/dimension_plugins.rs:90-98
Step 4: run the test that checks the count
Section titled “Step 4: run the test that checks the count”The test plugin prints nothing. The test asserts that the counter is above zero in both worlds, with ticks::<true>(...) in the same file. The command below runs this test, and the chat test uses the same pattern.4
cargo test -p mcrs_minecraft_server --test it dimension_plugins::We did not run this command for this page. The repository has no log line for this plugin, so this page shows no expected log. If you want one, add a tracing::info! call to your system.
The same plugin in Paper
Section titled “The same plugin in Paper”This is illustrative Java. It is not from the repository and it is not mcrs code.
public final class CountTicks extends JavaPlugin { private long ticks;
@Override public void onEnable() { getServer().getScheduler().runTaskTimer(this, () -> ticks++, 0L, 1L); }}The idea is the same, and three things differ. The mcrs plugin is compiled into the server program, while the Paper plugin ships as a jar. The counter is a resource in a world, where Paper keeps it in a field of your class. The task is a system that Bevy schedules, and it runs once in each dimension world.
What plugins cannot do yet
Section titled “What plugins cannot do yet”Four things a plugin may need are not ready yet. They are public events, chunk sources, saving and dimensions at run time.5678 Status shows what else is missing for plugins, and Dimensions explains why add_plugins does not reach a dimension.
Next: Intercept chat uses the first public event to cancel and rewrite a chat line.
Footnotes
Section titled “Footnotes”-
Undecided#321 Tracks making the protocol, network and game data crates publishable. ↩
-
WIP#288 Plans workspace members under
examples/with a dependency-tree test per example. ↩ -
Works#280 Lets user plugins run inside every dimension world. ↩
-
Works#636 Raises a cancellable event for a chat line before its broadcast. ↩
-
Partial#283 Tracks public player events that a plugin can cancel. ↩
-
WIP#281 Plans the chunk source as a replaceable plugin point, with empty and flat sources. ↩
-
WIP#546 Plans to create and remove dimension worlds at run time and move players between them. ↩
Sources and verification
Verified: 26.4-snapshot-7, 51d45c26a, 2026-11-09