483 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			Rust
		
	
	
	
			
		
		
	
	
			483 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			Rust
		
	
	
	
//! 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<Command>),
 | 
						|
    /// Comment to be added to the function
 | 
						|
    Comment(String),
 | 
						|
    /// Return value
 | 
						|
    Return(ReturnCommand),
 | 
						|
 | 
						|
    /// Command that is a concatenation of two commands
 | 
						|
    Concat(Box<Command>, Box<Command>),
 | 
						|
}
 | 
						|
 | 
						|
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<CompiledCommand> {
 | 
						|
        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<u8>) -> 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<Command>),
 | 
						|
}
 | 
						|
 | 
						|
fn compile_debug(message: &MacroString, option: &CompileOptions) -> Vec<CompiledCommand> {
 | 
						|
    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<CompiledCommand> {
 | 
						|
    let command_count = commands
 | 
						|
        .iter()
 | 
						|
        .map(|cmd| cmd.get_count(options))
 | 
						|
        .sum::<usize>();
 | 
						|
    // 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::<Vec<_>>()
 | 
						|
                    .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<u8>) -> bool {
 | 
						|
    static CMD_FORMATS: OnceLock<HashMap<&str, RangeInclusive<u8>>> = OnceLock::new();
 | 
						|
    let cmd_formats = CMD_FORMATS.get_or_init(|| {
 | 
						|
        const LATEST: u8 = Datapack::LATEST_FORMAT;
 | 
						|
        const ANY: RangeInclusive<u8> = 0..=LATEST;
 | 
						|
        const fn to(to: u8) -> RangeInclusive<u8> {
 | 
						|
            0..=to
 | 
						|
        }
 | 
						|
        const fn from(from: u8) -> RangeInclusive<u8> {
 | 
						|
            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)));
 | 
						|
    }
 | 
						|
}
 |