//! Represents a command that can be included in a function. mod execute; use std::{ collections::{HashMap, HashSet}, ops::RangeInclusive, sync::OnceLock, }; pub use execute::{Condition, Execute}; use chksum_md5 as md5; use super::Function; use crate::{ prelude::Datapack, util::{ compile::{CompileOptions, CompiledCommand, FunctionCompilerState, MutCompilerState}, MacroString, }, }; /// Represents a command that can be included in a function. #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum Command { /// A command that is already formatted as a string. Raw(String), /// A command that contains macro usages UsesMacro(MacroString), /// Message to be printed only in debug mode Debug(MacroString), /// Execute command Execute(Execute), /// Group of commands to be called instantly after each other Group(Vec), /// Comment to be added to the function Comment(String), /// Return value Return(ReturnCommand), /// Command that is a concatenation of two commands Concat(Box, Box), } impl Command { /// Create a new raw command. #[must_use] pub fn raw(command: &str) -> Self { Self::Raw(command.to_string()) } /// Compile the command into a string. pub fn compile( &self, options: &CompileOptions, global_state: &MutCompilerState, function_state: &FunctionCompilerState, ) -> Vec { match self { Self::Raw(command) => vec![CompiledCommand::new(command.clone())], Self::UsesMacro(command) => { vec![CompiledCommand::new(command.compile()).with_contains_macros(true)] } Self::Debug(message) => compile_debug(message, options), Self::Execute(ex) => ex.compile(options, global_state, function_state), Self::Group(commands) => compile_group(commands, options, global_state, function_state), Self::Comment(comment) => { vec![CompiledCommand::new("#".to_string() + comment).with_forbid_prefix(true)] } Self::Return(return_cmd) => match return_cmd { ReturnCommand::Value(value) => { vec![CompiledCommand::new(format!("return {}", value.compile()))] } ReturnCommand::Command(cmd) => { let compiled_cmd = Self::Group(vec![*cmd.clone()]).compile( options, global_state, function_state, ); let compiled_cmd = compiled_cmd .into_iter() .next() .expect("group will always return exactly one command"); vec![compiled_cmd.apply_prefix("return run ")] } }, Self::Concat(a, b) => { let a = a.compile(options, global_state, function_state); let b = b.compile(options, global_state, function_state); a.into_iter() .flat_map(|a| { b.iter().map(move |b| { if a.is_empty() { b.clone() } else if b.is_empty() { a.clone() } else { b.clone() .apply_prefix(a.as_str()) .or_forbid_prefix(a.forbids_prefix()) } }) }) .collect() } } } /// Get the count of the commands this command will compile into. #[must_use] fn get_count(&self, options: &CompileOptions) -> usize { match self { // TODO: change comment to compile to `1`, make sure nothing breaks Self::Comment(_) => 0, Self::Debug(_) => usize::from(options.debug), Self::Raw(cmd) => cmd.split('\n').count(), Self::UsesMacro(cmd) => cmd.line_count(), Self::Execute(ex) => ex.get_count(options), Self::Group(_) | Self::Return(_) => 1, Self::Concat(a, b) => a.get_count(options) + b.get_count(options) - 1, } } /// Check whether the command is valid with the given pack format. #[must_use] pub fn validate(&self, pack_formats: &RangeInclusive) -> bool { let command_valid = match self { Self::Comment(_) | Self::Debug(_) | Self::Group(_) => true, Self::Raw(cmd) => validate_raw_cmd(cmd, pack_formats), Self::UsesMacro(cmd) => validate_raw_cmd(&cmd.compile(), pack_formats), Self::Execute(ex) => ex.validate(pack_formats), Self::Return(ret) => match ret { ReturnCommand::Value(_) => pack_formats.start() >= &14, ReturnCommand::Command(cmd) => { pack_formats.start() >= &16 && cmd.validate(pack_formats) } }, Self::Concat(a, b) => a.validate(pack_formats) && b.validate(pack_formats), }; if pack_formats.start() < &16 { command_valid && !self.contains_macro() } else { command_valid } } /// Check whether the command contains a macro. #[must_use] pub fn contains_macro(&self) -> bool { match self { Self::Raw(_) | Self::Comment(_) => false, Self::UsesMacro(s) | Self::Debug(s) => s.contains_macro(), Self::Group(commands) => group_contains_macro(commands), Self::Execute(ex) => ex.contains_macro(), Self::Return(ret) => match ret { ReturnCommand::Value(value) => value.contains_macro(), ReturnCommand::Command(cmd) => cmd.contains_macro(), }, Self::Concat(a, b) => a.contains_macro() || b.contains_macro(), } } /// Returns the names of the macros used #[must_use] pub fn get_macros(&self) -> HashSet<&str> { match self { Self::Raw(_) | Self::Comment(_) => HashSet::new(), Self::UsesMacro(s) | Self::Debug(s) => s.get_macros(), Self::Group(commands) => group_get_macros(commands), Self::Execute(ex) => ex.get_macros(), Self::Return(ret) => match ret { ReturnCommand::Value(value) => value.get_macros(), ReturnCommand::Command(cmd) => cmd.get_macros(), }, Self::Concat(a, b) => { let mut macros = a.get_macros(); macros.extend(b.get_macros()); macros } } } /// Check whether the command should not have a prefix. #[must_use] pub fn forbid_prefix(&self) -> bool { match self { Self::Comment(_) => true, Self::Raw(_) | Self::Debug(_) | Self::Execute(_) | Self::UsesMacro(_) => false, Self::Group(commands) => commands.len() == 1 && commands[0].forbid_prefix(), Self::Return(ret) => match ret { ReturnCommand::Value(_) => false, ReturnCommand::Command(cmd) => cmd.forbid_prefix(), }, Self::Concat(a, _) => a.forbid_prefix(), } } } impl From<&str> for Command { fn from(command: &str) -> Self { Self::raw(command) } } impl From<&Function> for Command { fn from(value: &Function) -> Self { Self::Raw(format!("function {}:{}", value.namespace(), value.name())) } } impl From<&mut Function> for Command { fn from(value: &mut Function) -> Self { Self::Raw(format!("function {}:{}", value.namespace(), value.name())) } } /// Represents a command that returns a value. #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum ReturnCommand { /// Returns the value Value(MacroString), /// Returns the result of the command Command(Box), } fn compile_debug(message: &MacroString, option: &CompileOptions) -> Vec { if option.debug { vec![CompiledCommand::new(format!( r#"tellraw @a [{{"text":"[","color":"dark_blue"}},{{"text":"DEBUG","color":"dark_green","hoverEvent":{{"action":"show_text","value":[{{"text":"Debug message generated by Shulkerbox"}},{{"text":"\nSet debug to 'false' to disable"}}]}}}},{{"text":"] ","color":"dark_blue"}},{{"text":"{}","color":"black"}}]"#, message.compile() ))] } else { Vec::new() } } #[tracing::instrument(skip_all, fields(commands = ?commands))] fn compile_group( commands: &[Command], options: &CompileOptions, global_state: &MutCompilerState, function_state: &FunctionCompilerState, ) -> Vec { let command_count = commands .iter() .map(|cmd| cmd.get_count(options)) .sum::(); // only create a function if there are more than one command match command_count { 0 => Vec::new(), 1 => commands[0].compile(options, global_state, function_state), _ => { let uid = function_state.request_uid(); let pass_macros = group_contains_macro(commands); // calculate a hashed path for the function in the `sb` subfolder let function_path = { let function_path = function_state.path(); let function_path = function_path.strip_prefix("sb/").unwrap_or(function_path); let pre_hash_path = function_path.to_owned() + ":" + &uid.to_string(); let hash = md5::hash(pre_hash_path).to_hex_lowercase(); "sb/".to_string() + function_path + "/" + &hash[..16] }; let namespace = function_state.namespace(); // create a new function with the commands let mut function = Function::new(namespace, &function_path); function.get_commands_mut().extend(commands.iter().cloned()); function_state.add_function(&function_path, function); let mut function_invocation = format!("function {namespace}:{function_path}"); if pass_macros { // WARNING: this seems to be the only way to pass macros to the function called. // Because everything is passed as a string, it looses one "level" of escaping per pass. let macros_block = group_get_macros(commands) .into_iter() .map(|m| format!(r#"{m}:"$({m})""#)) .collect::>() .join(","); function_invocation.push_str(&format!(" {{{macros_block}}}")); } vec![CompiledCommand::new(function_invocation).with_contains_macros(pass_macros)] } } } fn group_contains_macro(commands: &[Command]) -> bool { commands.iter().any(Command::contains_macro) } fn group_get_macros(commands: &[Command]) -> HashSet<&str> { let mut macros = HashSet::new(); for cmd in commands { macros.extend(cmd.get_macros()); } macros } #[allow(clippy::too_many_lines)] fn validate_raw_cmd(cmd: &str, pack_formats: &RangeInclusive) -> bool { static CMD_FORMATS: OnceLock>> = OnceLock::new(); let cmd_formats = CMD_FORMATS.get_or_init(|| { const LATEST: u8 = Datapack::LATEST_FORMAT; const ANY: RangeInclusive = 0..=LATEST; const fn to(to: u8) -> RangeInclusive { 0..=to } const fn from(from: u8) -> RangeInclusive { from..=LATEST } const ANY_CMD: &[&str] = &[ "advancement", "ban", "ban-ip", "banlist", "clear", "clone", "debug", "defaultgamemode", "deop", "difficulty", "effect", "enchant", "execute", "experience", "fill", "gamemode", "gamerule", "give", "help", "kick", "kill", "list", "locate", "me", "msg", "op", "pardon", "pardon-ip", "particle", "playsound", "publish", "recipe", "reload", "save-all", "save-off", "save-on", "say", "scoreboard", "seed", "setblock", "setidletimeout", "setworldspawn", "spawnpoint", "spreadplayers", "stop", "stopsound", "summon", "teleport", "tell", "tellraw", "time", "title", "tp", "trigger", "w", "weather", "whitelist", "worldborder", "xp", ]; let mut map = HashMap::new(); for cmd in ANY_CMD { map.insert(*cmd, ANY); } map.insert("attribute", from(6)); map.insert("bossbar", from(4)); map.insert("damage", from(12)); map.insert("data", from(4)); map.insert("datapack", from(4)); map.insert("fillbiome", from(12)); map.insert("forceload", from(4)); map.insert("function", from(4)); map.insert("replaceitem", to(6)); map.insert("item", from(7)); map.insert("jfr", from(8)); map.insert("loot", from(4)); map.insert("perf", from(7)); map.insert("place", from(10)); map.insert("placefeature", 9..=9); map.insert("random", from(18)); map.insert("return", from(15)); map.insert("ride", from(12)); map.insert("schedule", from(4)); map.insert("spectate", from(5)); map.insert("tag", from(4)); map.insert("team", from(4)); map.insert("teammsg", from(4)); map.insert("tick", from(22)); map.insert("tm", from(4)); map.insert("transfer", from(41)); map }); cmd.split_ascii_whitespace().next().is_none_or(|cmd| { cmd_formats.get(cmd).is_none_or(|range| { let start_cmd = range.start(); let end_cmd = range.end(); let start_pack = pack_formats.start(); let end_pack = pack_formats.end(); start_cmd <= start_pack && end_cmd >= end_pack }) }) } #[cfg(test)] mod tests { use std::sync::Mutex; use crate::util::compile::CompilerState; use super::*; #[test] fn test_raw() { let command_a = Command::Raw("say Hello, world!".to_string()); let command_b = Command::raw("say foo bar"); let options = &CompileOptions::default(); let global_state = &Mutex::new(CompilerState::default()); let function_state = &FunctionCompilerState::default(); assert_eq!( command_a.compile(options, global_state, function_state), vec![CompiledCommand::new("say Hello, world!")] ); assert_eq!(command_a.get_count(options), 1); assert_eq!( command_b.compile(options, global_state, function_state), vec![CompiledCommand::new("say foo bar")] ); assert_eq!(command_b.get_count(options), 1); } #[test] fn test_comment() { let comment = Command::Comment("this is a comment".to_string()); let options = &CompileOptions::default(); let global_state = &Mutex::new(CompilerState::default()); let function_state = &FunctionCompilerState::default(); assert_eq!( comment.compile(options, global_state, function_state), vec![CompiledCommand::new("#this is a comment").with_forbid_prefix(true)] ); assert_eq!(comment.get_count(options), 0); } #[test] fn test_validate() { let tag = Command::raw("tag @s add foo"); assert!(tag.validate(&(6..=9))); assert!(!tag.validate(&(2..=5))); let kill = Command::raw("kill @p"); assert!(kill.validate(&(2..=40))); } }