Skip to content

feat: implement /say, /me, and /msg chat commands - #302

Open
nglmercer wants to merge 20 commits into
Steel-Foundation:masterfrom
nglmercer:feat/message-commands
Open

feat: implement /say, /me, and /msg chat commands#302
nglmercer wants to merge 20 commits into
Steel-Foundation:masterfrom
nglmercer:feat/message-commands

Conversation

@nglmercer

Copy link
Copy Markdown

Summary

Implements the vanilla chat messaging commands:

  • /say <message> — operator-only, broadcasts to all players and logs to the server console.
  • /me <message> — available to all players, broadcasts an emote.
  • /msg <targets> <message> — available to all players, with /tell and /w aliases. Sends an incoming whisper to each target and an outgoing echo back to the sender.

Command-generated chat is delivered as unsigned disguised chat (CDisguisedChat) bound to the matching vanilla chat type (say_command, emote_command, msg_command_incoming, msg_command_outgoing), so the client applies the standard decorations ([%s] %s, * %s %s, %s whispers to you: %s, You whisper to %s: %s).

Changes

  • steel-core/src/command/builtins/message.rs — new module with the three commands and graph-shape tests.
  • steel-core/src/command/execution/text.rs — adds CommandTextResolver::for_source for resolving a message once against the command source.
  • steel-core/src/command/execution/source.rs — removes the now-used dead_code expectation on CommandSource::sender().
  • steel-core/src/command/builtins/mod.rs — registers the commands and extends the command graph-shape test.

Verification

  • cargo check -p steel-core — pass
  • cargo fmt --all --check — clean
  • cargo clippy -p steel-core --all-targets --all-features — 0 warnings
  • cargo test -p steel-core --lib command::builtins — 72 passed, including 3 new tests and the updated graph-shape test

Note on vanilla verification

The chat-type bindings and decoration parameters were taken from the repository's generated data (vanilla_chat_types.rs and the generated translations) rather than transcribed from memory. minecraft-src/ was not generated locally, so the /msg outgoing-echo binding follows stable vanilla behavior corroborated by the msg_command_outgoing parameter data ([target, content]); it can be confirmed against MessageCommand.java once the vanilla source is generated.

Add the vanilla chat messaging commands /say, /me, and /msg (with /tell and /w aliases). Command-generated chat is delivered as unsigned disguised chat bound to the matching vanilla chat type (say_command, emote_command, msg_command_incoming/outgoing), so the client applies the usual decorations.

/say is operator-only and broadcasts server-wide; /me and /msg are available to all players. Also adds a CommandTextResolver::for_source helper for resolving a message once against the command source.
@nglmercer

Copy link
Copy Markdown
Author

no existing handling (or test) for multi-word component arguments

Plain multi-word input is now consumed whole as literal text instead of failing on the first space, so /msg, /say, and /me accept real sentences. Input that opens a structured SNBT component (a leading {, [, or quote) still parses through the SNBT path unchanged, which also lets /tellraw accept plain text. Selector and score resolution are unaffected because those only come from structured components.
@nglmercer

Copy link
Copy Markdown
Author

is added a new multi-word component arguments starts_structured_component and parse_structured_component.

reader.restore(start);
invalid_component(reader, error.to_string())
})?;
validate_component_syntax(&component).map_err(|error| {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why have remove the validate ?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"parse_component was not removed and remains unchanged. Since I introduced parse_structured_component, we can add validate_component_syntax there instead. What do you think?"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm ok not really familiar with it, will try do a more deep review later

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@coco875 did you had time for the deeper review?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not really an expert in command but can try

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same for me

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@hthug06 if you have time and want to help, I know you did commands, so I think you are more expert than us maybe you can find something

nglmercer and others added 3 commits August 2, 2026 11:21
@JunkyDeveloper

Copy link
Copy Markdown
Contributor

Please fix lint

@nglmercer

Copy link
Copy Markdown
Author

Please fix lint

ready

coco875
coco875 previously approved these changes Aug 5, 2026
Comment on lines +49 to +62
fn say_command() -> CommandNodeBuilder<CommandSource, SteelCommandRuntime> {
literal("say").then(argument("message", SteelArgumentType::component()).executes(say))
}

fn me_command() -> CommandNodeBuilder<CommandSource, SteelCommandRuntime> {
literal("me").then(argument("message", SteelArgumentType::component()).executes(me))
}

fn msg_command() -> CommandNodeBuilder<CommandSource, SteelCommandRuntime> {
literal("msg").then(
argument("targets", SteelArgumentType::players())
.then(argument("message", SteelArgumentType::component()).executes(msg)),
)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm pretty sure the wrong argument type is being used. It should be some kind of MessageArgument (from Vanilla), not a text component.

@nglmercer nglmercer Aug 5, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm pretty sure the wrong argument type is being used. It should be some kind of MessageArgument (from Vanilla), not a text component.

ready new 'message' in SteelArgumentType

Introduces `SteelArgumentType::message()` and `MessageParser` that parse
the entire remaining input as plain text with vanilla's 256-character
limit, mirroring `MessageArgument.parseText`. The `say`, `me`, and `msg`
commands now use this instead of the structured component parser, which
matches vanilla behavior where these commands accept raw text rather
than JSON components.
The spawned domain-switch storage tasks run on the test's current-thread
runtime, so the loop yields to the runtime each tick to drive their
async
I/O. A periodic sleep lets the runtime process I/O completions; without
it, slow disk writes under parallel test load could exhaust the tick
budget.
@nglmercer
nglmercer requested a review from Laptop59 August 5, 2026 17:49

@Laptop59 Laptop59 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I actually tested this PR, and I found out that when trying to run /say, /tell, or /me as a player in the server, nothing seems to happen. This is because the server does not currently know how to handle the packet SChatCommandSigned. The packet's ReadFrom implementation is already there, so it just needs to be handled when caught, which is currently not done in SteelMC, as indicated by

[Info] play packet id 8 is not known

Seems like on the server terminal, the commands work as expected, except for the selectors resolving to entity names.

Comment on lines +1196 to +1208
/// Parses a vanilla message argument: the entire remaining input as plain text.
///
/// Mirrors `MessageArgument.parseText`, including the 256-character length limit.
fn parse_message(reader: &mut StringReader<'_>) -> Result<TextComponent, CommandSyntaxError> {
let length = reader.remaining().len();
if length > 256 {
let message = translations::ARGUMENT_MESSAGE_TOO_LONG
.message([length.to_string(), "256".to_owned()])
.component();
return Err(reader.error(CommandSyntaxErrorKind::Dynamic(Box::new(message))));
}
Ok(TextComponent::plain(reader.read_remaining().to_owned()))
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Vanilla also resolves selectors to the names of the entities referenced by them, separated by commas, if the sender is allowed to use them.
The current code for parsing here does not do this and simply just reads the remaining string.

Comment thread steel-core/src/server/tests.rs Outdated
Comment on lines +1254 to +1277
/// Advances chunk scheduling and polls server jobs until the queue empties.
///
/// The spawned domain-switch storage tasks run on the test's current-thread
/// runtime, so the loop yields to the runtime each tick to drive their async
/// I/O. A periodic sleep lets the runtime process I/O completions; without it,
/// slow disk writes under parallel test load could exhaust the tick budget.
async fn tick_until_jobs_finish(server: &Arc<Server>, worlds: &[Arc<World>]) {
for tick in 1..=1_000_000u64 {
for world in worlds {
world.chunk_map.advance_scheduling();
}
server.tick_jobs(tick, true);
if server.jobs.is_empty() {
return;
}
if tick.is_multiple_of(100) {
sleep(Duration::from_millis(1)).await;
} else {
yield_now().await;
}
}
panic!("server jobs did not finish within 1_000_000 ticks");
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do a chunk scheduling change in a command PR? Isn't it out of scope?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it isn't chunk scheduling only test, but also don't see much benefit

@coco875

coco875 commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

@Laptop59 thanks for the help to review the pr

@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown

This pull request has conflicts with the base branch "master". Please resolve those so we can test out your changes.

Comment thread steel-core/src/server/tests.rs Outdated
Comment on lines +1254 to +1277
/// Advances chunk scheduling and polls server jobs until the queue empties.
///
/// The spawned domain-switch storage tasks run on the test's current-thread
/// runtime, so the loop yields to the runtime each tick to drive their async
/// I/O. A periodic sleep lets the runtime process I/O completions; without it,
/// slow disk writes under parallel test load could exhaust the tick budget.
async fn tick_until_jobs_finish(server: &Arc<Server>, worlds: &[Arc<World>]) {
for tick in 1..=1_000_000u64 {
for world in worlds {
world.chunk_map.advance_scheduling();
}
server.tick_jobs(tick, true);
if server.jobs.is_empty() {
return;
}
if tick.is_multiple_of(100) {
sleep(Duration::from_millis(1)).await;
} else {
yield_now().await;
}
}
panic!("server jobs did not finish within 1_000_000 ticks");
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it isn't chunk scheduling only test, but also don't see much benefit

@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown

Conflicts have been resolved! 🎉

MessageValue now stores raw text and parsed selector parts, resolving
them
via CommandTextResolutionSource when the message is delivered. This
mirrors
vanilla's MessageArgument behavior where selectors are only resolved
against
the sender at delivery time.

A new try_parse_message_selector handles message args specifically,
treating
invalid selector types (e.g. @x) as literal text instead of errors.

Inline the tick_until_jobs_finish test helper.
Message arguments now scan for `@a`/`@p`/`@r`/`@s`/`@e` selectors during
parsing when the source allows them, matching vanilla
`MessageArgument.parseText` behavior. Selectors are stored as parts on
`MessageValue` and resolved to entity names only at delivery time.
Replace nonexistent steel_registry::test_support::init_test_registry with
the actual steel_registry::init_vanilla_registry. Suppress dead_code on
CommandTextResolver::for_source which is kept as a utility API.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants