feat: implement /say, /me, and /msg chat commands - #302
Conversation
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.
|
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.
|
is added a new multi-word component arguments |
| reader.restore(start); | ||
| invalid_component(reader, error.to_string()) | ||
| })?; | ||
| validate_component_syntax(&component).map_err(|error| { |
There was a problem hiding this comment.
why have remove the validate ?
There was a problem hiding this comment.
"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?"
There was a problem hiding this comment.
hmm ok not really familiar with it, will try do a more deep review later
There was a problem hiding this comment.
@coco875 did you had time for the deeper review?
There was a problem hiding this comment.
not really an expert in command but can try
There was a problem hiding this comment.
@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
Add validation for component syntax and error handling.
Removed redundant validation of component syntax.
|
Please fix lint |
ready |
| 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)), | ||
| ) | ||
| } |
There was a problem hiding this comment.
I'm pretty sure the wrong argument type is being used. It should be some kind of MessageArgument (from Vanilla), not a text component.
There was a problem hiding this comment.
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.
Laptop59
left a comment
There was a problem hiding this comment.
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.
| /// 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())) | ||
| } |
There was a problem hiding this comment.
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.
| /// 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"); | ||
| } | ||
|
|
There was a problem hiding this comment.
Why do a chunk scheduling change in a command PR? Isn't it out of scope?
There was a problem hiding this comment.
it isn't chunk scheduling only test, but also don't see much benefit
|
@Laptop59 thanks for the help to review the pr |
|
This pull request has conflicts with the base branch "master". Please resolve those so we can test out your changes. |
| /// 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"); | ||
| } | ||
|
|
There was a problem hiding this comment.
it isn't chunk scheduling only test, but also don't see much benefit
|
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.
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/telland/waliases. 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— addsCommandTextResolver::for_sourcefor resolving a message once against the command source.steel-core/src/command/execution/source.rs— removes the now-useddead_codeexpectation onCommandSource::sender().steel-core/src/command/builtins/mod.rs— registers the commands and extends the command graph-shape test.Verification
cargo check -p steel-core— passcargo fmt --all --check— cleancargo clippy -p steel-core --all-targets --all-features— 0 warningscargo test -p steel-core --lib command::builtins— 72 passed, including 3 new tests and the updated graph-shape testNote on vanilla verification
The chat-type bindings and decoration parameters were taken from the repository's generated data (
vanilla_chat_types.rsand the generated translations) rather than transcribed from memory.minecraft-src/was not generated locally, so the/msgoutgoing-echo binding follows stable vanilla behavior corroborated by themsg_command_outgoingparameter data ([target, content]); it can be confirmed againstMessageCommand.javaonce the vanilla source is generated.