Conversation
|
We now have a new command system. |
- Generate random integers with value (private) and roll (broadcast) modes - Named sequences with deterministic xoroshiro128 RNG - Sequence reset with seed, includeWorldSeed, includeSequenceId options - Strict namespaced ID validation matching vanilla error positions - Tab completion for existing sequence names - Sequence state persisted in level.dat via RandomSequence in LevelData
ff02045 to
4bec4a8
Compare
|
Ported to the new command system as requested. |
|
Hey, Thank you for your PR
I think all Arg consumers you need are already implemented |
|
Fixed both review points:
|
|
Doesn't this go back to the old command dispatcher? |
Laptop59
left a comment
There was a problem hiding this comment.
Overall, this PR still needs some work to be done before it has the potential to be merged.
| struct RangeArgumentType; | ||
|
|
||
| impl ArgumentType for RangeArgumentType { | ||
| type Item = (i32, i32); | ||
|
|
||
| fn parse(&self, reader: &mut StringReader) -> Result<Self::Item, CommandSyntaxError> { | ||
| let start = reader.cursor(); | ||
|
|
||
| let min = if reader.peek() == Some('.') { | ||
| None | ||
| } else { | ||
| Some(reader.read_int()?) | ||
| }; | ||
|
|
||
| let has_separator = reader.peek() == Some('.') && reader.peek_with_offset(1) == Some('.'); | ||
|
|
||
| let (min, max) = if has_separator { | ||
| reader.expect('.')?; | ||
| reader.expect('.')?; | ||
|
|
||
| let max = match reader.peek() { | ||
| None => None, | ||
| Some(c) if c.is_whitespace() => None, | ||
| _ => Some(reader.read_int()?), | ||
| }; | ||
|
|
||
| (min.unwrap_or(i32::MIN), max.unwrap_or(i32::MAX)) | ||
| } else { | ||
| let Some(value) = min else { | ||
| reader.set_cursor(start); | ||
| return Err(error_types::READER_EXPECTED_INT.create(reader)); | ||
| }; | ||
|
|
||
| (value, value) | ||
| }; | ||
|
|
||
| let range_size = i64::from(max) - i64::from(min) + 1; | ||
| if range_size < 2 { | ||
| return Err( | ||
| error_types::DISPATCHER_PARSE_EXCEPTION.create_without_context( | ||
| TextComponent::translate( | ||
| translation::COMMANDS_RANDOM_ERROR_RANGE_TOO_SMALL, | ||
| [], | ||
| ), | ||
| ), | ||
| ); | ||
| } | ||
| if range_size > 2_147_483_646 { | ||
| return Err( | ||
| error_types::DISPATCHER_PARSE_EXCEPTION.create_without_context( | ||
| TextComponent::translate( | ||
| translation::COMMANDS_RANDOM_ERROR_RANGE_TOO_LARGE, | ||
| [], | ||
| ), | ||
| ), | ||
| ); | ||
| } | ||
|
|
||
| Ok((min, max)) | ||
| } | ||
|
|
||
| fn client_side_parser(&'_ self) -> JavaClientArgumentType<'_> { | ||
| JavaClientArgumentType::String(StringProtoArgBehavior::SingleWord) | ||
| } | ||
| } |
There was a problem hiding this comment.
You should remove this part of the code as the RangeArgumentType has been implemented in the argument_types module, which can also be used by other commands.
There was a problem hiding this comment.
Removed, switched to shared IntRangeArgumentType. Kept the /random-specific size checks (>= 2, <= 2_147_483_646) in the executor since those are command constraints, not general range constraints.
| @@ -148,13 +209,13 @@ impl InclusiveRange { | |||
|
|
|||
| let range_size = i64::from(max) - i64::from(min) + 1; | |||
| if range_size < 2 { | |||
| return Err(CommandFailed(TextComponent::translate( | |||
| return Err(CommandError::CommandFailed(TextComponent::translate( | |||
| translation::COMMANDS_RANDOM_ERROR_RANGE_TOO_SMALL, | |||
| [], | |||
| ))); | |||
| } | |||
| if range_size > 2_147_483_646 { | |||
| return Err(CommandFailed(TextComponent::translate( | |||
| return Err(CommandError::CommandFailed(TextComponent::translate( | |||
| translation::COMMANDS_RANDOM_ERROR_RANGE_TOO_LARGE, | |||
| [], | |||
| ))); | |||
| @@ -164,9 +225,10 @@ impl InclusiveRange { | |||
| } | |||
| } | |||
There was a problem hiding this comment.
This should be removed as it's an artifact of the previous commits in this PR, and that we already have the argument type required.
| #[cfg(test)] | ||
| fn parse_i32_range_bound(raw: &str) -> Result<i32, CommandError> { | ||
| raw.parse::<i32>().map_err(|_| { | ||
| CommandError::CommandFailed(TextComponent::translate( | ||
| translation::PARSING_INT_INVALID, | ||
| [TextComponent::text(raw.to_string())], | ||
| )) | ||
| }) | ||
| } |
There was a problem hiding this comment.
Uses the older CommandError, was this supposed to be removed?
| fn get_optional_argument<'a, T: 'static>( | ||
| context: &'a CommandContext, | ||
| name: &str, | ||
| ) -> Result<Option<&'a T>, CommandSyntaxError> { | ||
| if context.arguments.contains_key(name) { | ||
| context.get_argument(name).map(Some) | ||
| } else { | ||
| Ok(None) | ||
| } | ||
| } |
There was a problem hiding this comment.
A utility method for optionally parsing an argument is 'very hacky'. The executor should know what arguments to parse and what arguments not to parse after command parsing from its stored values
There was a problem hiding this comment.
Removed. ResetExecutor now uses explicit ResetMode variants wired from the command tree, so each path knows exactly which arguments to parse.
| fn parse_sequence_arg<'a>(context: &'a CommandContext) -> Result<&'a str, CommandSyntaxError> { | ||
| let sequence = context.get_argument::<String>(ARG_SEQUENCE)?; | ||
| let raw_arg = RawArg { | ||
| value: sequence, | ||
| start: 0, | ||
| end: sequence.len(), | ||
| input: sequence, | ||
| }; | ||
|
|
||
| validate_sequence_name(raw_arg) | ||
| } |
There was a problem hiding this comment.
The sequence argument is an identifier (formerly resource location) containing a namespace and path, not a string. We don't have parsing for identifiers yet. A way to parse them will be required before this argument can be implemented properly.
There was a problem hiding this comment.
Added ResourceLocationArgumentType to argument_types/core and used it for ARG_SEQUENCE. Client-side parser is now ResourceLocation. validate_sequence_name() stays in the executor for server-side validation.
| fn level_two_requirement( | ||
| source: &CommandSource, | ||
| ) -> std::pin::Pin<Box<dyn std::future::Future<Output = bool> + Send + '_>> { | ||
| Box::pin(async move { source.output.has_permission_lvl(PermissionLvl::Two) }) | ||
| } |
There was a problem hiding this comment.
Instead of checking if the sender has permission level 2, we should create a permission and check if it has that, so that the permission level of it can be changed as desired (by server owners, for example) without conflicts.
There was a problem hiding this comment.
Replaced with explicit permissions minecraft:command.random.sequence and minecraft:command.random.reset, both registered with Op(Two) and checked via .requires(...)
| #[test] | ||
| fn parse_valid_closed_range() { | ||
| let range = InclusiveRange::parse("1..10").expect("range should parse"); | ||
| assert_eq!(range.min, 1); | ||
| assert_eq!(range.max, 10); | ||
| } | ||
|
|
||
| #[test] | ||
| fn parse_valid_open_lower_bound_range() { | ||
| let range = InclusiveRange::parse("..-2147483647").expect("range should parse"); | ||
| assert_eq!(range.min, i32::MIN); | ||
| assert_eq!(range.max, -2_147_483_647); | ||
| } | ||
|
|
||
| #[test] | ||
| fn parse_valid_open_upper_bound_range() { | ||
| let range = InclusiveRange::parse("2147483646..").expect("range should parse"); | ||
| assert_eq!(range.min, 2_147_483_646); | ||
| assert_eq!(range.max, i32::MAX); | ||
| } | ||
|
|
||
| #[test] | ||
| fn reject_single_value_range() { | ||
| assert!(matches!( | ||
| InclusiveRange::parse("5"), | ||
| Err(CommandError::CommandFailed(_)) | ||
| )); | ||
| } | ||
|
|
||
| #[test] | ||
| fn reject_reversed_range() { | ||
| assert!(matches!( | ||
| InclusiveRange::parse("10..1"), | ||
| Err(CommandError::CommandFailed(_)) | ||
| )); | ||
| } | ||
|
|
||
| #[test] | ||
| fn reject_too_large_range_size() { | ||
| assert!(matches!( | ||
| InclusiveRange::parse("-2147483648..2147483647"), | ||
| Err(CommandError::CommandFailed(_)) | ||
| )); | ||
| } |
There was a problem hiding this comment.
The range module in argument_types has tests for parsing different types of ranges, so we don't need this part of the code.
There was a problem hiding this comment.
Removed, range tests are covered in argument_types/range.
| let mut path_raw_args = raw_args.clone(); | ||
| match Self::try_find_suggestions_on_path( | ||
| src, | ||
| server, | ||
| &path, | ||
| tree, | ||
| &mut path_raw_args, | ||
| cmd, | ||
| ) | ||
| .await |
There was a problem hiding this comment.
These changes look like an artifact of previous commits of this PR with the older dispatcher. raw_args was made mutable during those changes and probably should be reverted to be immutable again.
There was a problem hiding this comment.
Reverted dispatcher.rs to master baseline.
|
@Pa-dej There should be now an Identifer in master already, also please fix the conflict |
Description
Implements the vanilla
/randomcommand.Supported syntax:
/random value <range>- result visible only to sender/random roll <range>- result broadcast to all players/random value <range> <sequence>- sample from named sequence/random roll <range> <sequence>- same with broadcast/random reset <sequence> [seed] [includeWorldSeed] [includeSequenceId]/random reset *- reset all sequencesImplementation notes:
error position matches vanilla behavior.
(same pattern as GameRules - vanilla uses separate random_sequences.dat,
can be migrated in follow-up PR once world/data/ infrastructure exists)
Testing
cargo fmt,cargo clippy,cargo test- all pass.