Commands
Plugins register commands through the CommandManager. When a player types /yourcommand in chat, the proxy intercepts it, parses the arguments, and calls your handler. The message is never forwarded to the backend server.
Registering a command
Call command_manager().register() during on_enable:
ctx.command_manager().register(
"hello", // command name
&["hi", "hey"], // aliases
"Says hello to the player", // description
Box::new(HelloCommand), // handler
);2
3
4
5
6
Command names and aliases are case-insensitive. The proxy converts them to lowercase internally.
To remove a command at runtime:
ctx.command_manager().unregister("hello");Unregistering also cleans up all aliases for that command.
CommandHandler
Implement the CommandHandler trait on a struct. The execute method receives a CommandContext and a reference to the PlayerRegistry.
use infrarust_api::prelude::*;
struct HelloCommand;
impl CommandHandler for HelloCommand {
fn execute<'a>(
&'a self,
ctx: CommandContext,
player_registry: &'a dyn PlayerRegistry,
) -> BoxFuture<'a, ()> {
Box::pin(async move {
if let Some(id) = ctx.player_id
&& let Some(player) = player_registry.get_player_by_id(id)
{
let _ = player.send_message(
Component::text("Hello from Infrarust! ")
.color("gold")
.bold()
.append(Component::text("Welcome to the proxy.").color("gray")),
);
}
})
}
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
The return type is BoxFuture<'a, ()>. Wrap your async block with Box::pin(async move { ... }).
CommandContext
Every handler receives a CommandContext:
| Field | Type | Description |
|---|---|---|
player_id | Option<PlayerId> | The player who ran the command. None for console commands |
args | Vec<String> | Arguments split by whitespace |
raw | String | The full command string as typed |
If a player types /changepassword oldpass newpass, your handler receives:
args[0]="oldpass"args[1]="newpass"raw="changepassword oldpass newpass"
Parsing arguments
Arguments arrive as a Vec<String>. Check args.len() before accessing by index, and send a usage message if the player provides too few:
impl CommandHandler for ChangePasswordCommand {
fn execute<'a>(
&'a self,
ctx: CommandContext,
player_registry: &'a dyn PlayerRegistry,
) -> BoxFuture<'a, ()> {
Box::pin(async move {
let Some(player_id) = ctx.player_id else { return };
let Some(player) = player_registry.get_player_by_id(player_id) else {
return;
};
if ctx.args.len() < 2 {
let _ = player.send_message(
Component::text("Usage: /changepassword <old> <new>").color("red"),
);
return;
}
let old_password = &ctx.args[0];
let new_password = &ctx.args[1];
// ... validate and update password
})
}
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
Tab completion
Override tab_complete to provide suggestions when a player presses Tab. The default implementation returns no suggestions.
impl CommandHandler for MyCommand {
fn execute<'a>(
&'a self,
ctx: CommandContext,
player_registry: &'a dyn PlayerRegistry,
) -> BoxFuture<'a, ()> {
Box::pin(async move { /* ... */ })
}
fn tab_complete(&self, partial_args: &[&str]) -> Vec<String> {
match partial_args.len() {
0 | 1 => vec!["survival".into(), "creative".into(), "lobby".into()],
_ => vec![],
}
}
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
partial_args contains what the player has typed so far, split by whitespace.
Sharing state with a handler
Commands often need access to plugin state. Store shared data in an Arc field on the handler struct:
use std::sync::Arc;
pub struct ChangePasswordCommand {
pub handler: Arc<AuthHandler>,
}
impl CommandHandler for ChangePasswordCommand {
fn execute<'a>(
&'a self,
ctx: CommandContext,
player_registry: &'a dyn PlayerRegistry,
) -> BoxFuture<'a, ()> {
Box::pin(async move {
let config = self.handler.config();
let storage = self.handler.storage();
// ... use shared state
})
}
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Register it with Arc::clone:
ctx.command_manager().register(
"changepassword",
&["changepw", "cp"],
"Change your auth password",
Box::new(ChangePasswordCommand {
handler: Arc::clone(&handler),
}),
);2
3
4
5
6
7
8
Permission checks
The PlayerRegistry gives you access to the player object, which has a has_permission method:
fn is_admin(
player_id: PlayerId,
player_registry: &dyn PlayerRegistry,
handler: &AuthHandler,
) -> bool {
if let Some(player) = player_registry.get_player_by_id(player_id) {
if player.has_permission("auth.admin") {
return true;
}
handler.is_admin(&player.profile().username)
} else {
false
}
}2
3
4
5
6
7
8
9
10
11
12
13
14
Use this inside your handler to gate admin commands:
let Some(player_id) = ctx.player_id else { return };
if !is_admin(player_id, player_registry, &self.handler) {
if let Some(player) = player_registry.get_player_by_id(player_id) {
let _ = player.send_message(Component::error("No permission."));
}
return;
}2
3
4
5
6
7
Organizing commands
For plugins with multiple commands, put each handler in its own module and register them from a single function:
pub mod changepassword;
pub mod forcelogin;
pub mod unregister;
pub fn register_commands(ctx: &dyn PluginContext, handler: Arc<AuthHandler>) {
ctx.command_manager().register(
"changepassword",
&["changepw", "cp"],
"Change your auth password",
Box::new(changepassword::ChangePasswordCommand {
handler: Arc::clone(&handler),
}),
);
ctx.command_manager().register(
"forcelogin",
&[],
"Force-authenticate a player in auth limbo",
Box::new(forcelogin::ForceLoginCommand {
handler: Arc::clone(&handler),
}),
);
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Then call register_commands(ctx, handler) from your on_enable.
Dispatch flow
When a player sends a chat message starting with /:
- The proxy strips the leading
/and splits the input by whitespace. - The first token is the command name, converted to lowercase.
- If the name matches an alias, it resolves to the canonical command name.
- If a handler is registered for that name, the proxy builds a
CommandContextand callshandler.execute(). - The handler runs asynchronously. The message is not forwarded to the backend.
- If no handler matches, the message passes through to the backend server as normal.