From 89709834da6f39840caa9c6a2eadbdececdc7d44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20H=C3=B6lting?= <87192362+moritz-hoelting@users.noreply.github.com> Date: Tue, 2 Sep 2025 18:00:26 +0200 Subject: [PATCH] add group options for always extracting and manually loading macros --- src/datapack/command/execute/conditional.rs | 13 +- src/datapack/command/execute/mod.rs | 11 +- src/datapack/command/mod.rs | 491 ++++++++++++++------ src/datapack/mod.rs | 4 +- 4 files changed, 359 insertions(+), 160 deletions(-) diff --git a/src/datapack/command/execute/conditional.rs b/src/datapack/command/execute/conditional.rs index 19c5489..a1bd9bf 100644 --- a/src/datapack/command/execute/conditional.rs +++ b/src/datapack/command/execute/conditional.rs @@ -5,6 +5,7 @@ use std::{ }; use crate::{ + datapack::command::Group, prelude::Command, util::{ compile::{CompileOptions, CompiledCommand, FunctionCompilerState, MutCompilerState}, @@ -78,7 +79,7 @@ fn compile_using_data_storage( #[allow(clippy::option_if_let_else)] let then = if let Some(success_uid) = require_grouping_uid.as_deref() { // prepare commands for grouping - let mut group_cmd = match then.clone() { + let mut group_cmds = match then.clone() { Execute::Run(cmd) => vec![*cmd], Execute::Runs(cmds) => cmds, ex => vec![Command::Execute(ex)], @@ -86,13 +87,13 @@ fn compile_using_data_storage( // add success condition to the group // this condition will be checked after the group ran to determine if the else part should be executed if el.is_some() && str_cond.len() <= 1 { - group_cmd.push( + group_cmds.push( format!("data modify storage shulkerbox:cond {success_uid} set value true") .as_str() .into(), ); } - let group = Command::Group(group_cmd); + let group = Command::Group(Group::new(group_cmds)); let allows_prefix = !group.forbid_prefix(); group .compile(options, global_state, function_state) @@ -227,7 +228,7 @@ fn compile_since_20_format( global_state, function_state, ); - let group = Command::Group(group_cmds); + let group = Command::Group(Group::new(group_cmds)); let cmds = group.compile(options, global_state, function_state); if contains_macros { cmds.into_iter() @@ -254,7 +255,7 @@ fn compile_since_20_format( Execute::Runs(cmds) => cmds, ex => vec![Command::Execute(ex)], }; - let group_cmd = Command::Group(then_cmds); + let group_cmd = Command::Group(Group::new(then_cmds)); let then_cmd = if group_cmd.forbid_prefix() { group_cmd } else { @@ -346,7 +347,7 @@ fn handle_return_group_case_since_20( Execute::Runs(cmds) => cmds, ex => vec![Command::Execute(ex)], }; - let group = Command::Group(then_cmd); + let group = Command::Group(Group::new(then_cmd)); let then_cmd_concat = if group.forbid_prefix() { group } else { diff --git a/src/datapack/command/execute/mod.rs b/src/datapack/command/execute/mod.rs index 9536172..68d58b4 100644 --- a/src/datapack/command/execute/mod.rs +++ b/src/datapack/command/execute/mod.rs @@ -1,9 +1,12 @@ use std::{collections::HashSet, ops::RangeInclusive, string::ToString}; use super::Command; -use crate::util::{ - compile::{CompileOptions, CompiledCommand, FunctionCompilerState, MutCompilerState}, - ExtendableQueue, MacroString, +use crate::{ + datapack::command::Group, + util::{ + compile::{CompileOptions, CompiledCommand, FunctionCompilerState, MutCompilerState}, + ExtendableQueue, MacroString, + }, }; mod conditional; @@ -162,7 +165,7 @@ impl Execute { }) .collect(), Self::Runs(commands) => { - let group = Command::Group(commands.clone()); + let group = Command::Group(Group::new(commands.clone())); group .compile(options, global_state, function_state) .into_iter() diff --git a/src/datapack/command/mod.rs b/src/datapack/command/mod.rs index e38167b..ac520d5 100644 --- a/src/datapack/command/mod.rs +++ b/src/datapack/command/mod.rs @@ -3,6 +3,7 @@ mod execute; use std::{ collections::{HashMap, HashSet}, + hash::Hash, ops::RangeInclusive, sync::LazyLock, }; @@ -33,7 +34,7 @@ pub enum Command { /// Execute command Execute(Execute), /// Group of commands to be called instantly after each other - Group(Vec), + Group(Group), /// Comment to be added to the function Comment(String), /// Return value @@ -64,7 +65,7 @@ impl Command { } 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::Group(group) => group.compile(options, global_state, function_state), Self::Comment(comment) => { vec![CompiledCommand::new("#".to_string() + comment).with_forbid_prefix(true)] } @@ -101,7 +102,8 @@ impl Command { 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::Group(group) => group.get_count(options), + Self::Return(_) => 1, Self::Concat(a, b) => a.get_count(options) + b.get_count(options) - 1, } } @@ -135,7 +137,7 @@ impl Command { match self { Self::Raw(_) | Self::Comment(_) => false, Self::UsesMacro(s) | Self::Debug(s) => s.contains_macro(), - Self::Group(commands) => group_contains_macro(commands), + Self::Group(group) => group.contains_macro(), Self::Execute(ex) => ex.contains_macro(), Self::Return(ret) => match ret { ReturnCommand::Value(value) => value.contains_macro(), @@ -151,7 +153,7 @@ impl Command { 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::Group(group) => group.get_macros(), Self::Execute(ex) => ex.get_macros(), Self::Return(ret) => match ret { ReturnCommand::Value(value) => value.get_macros(), @@ -171,7 +173,7 @@ impl Command { 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::Group(group) => group.forbid_prefix(), Self::Return(ret) => match ret { ReturnCommand::Value(_) => false, ReturnCommand::Command(cmd) => cmd.forbid_prefix(), @@ -190,7 +192,7 @@ impl Command { Self::Execute(exec) => exec.contains_return(), Self::Raw(cmd) => cmd.starts_with("return "), Self::UsesMacro(m) => m.compile().starts_with("return "), - Self::Group(g) => g.iter().any(Self::contains_return), + Self::Group(g) => g.contains_return(), } } } @@ -211,6 +213,335 @@ impl From<&mut Function> for Command { } } +/// Represents a group of commands to be executed in sequence. +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Group { + /// The commands in the group. + commands: Vec, + /// Whether to always create a function for this group, even if it contains only one command. + always_create_function: bool, + /// Optional name for the data storage used for arguments. + data_storage_name: Option, + /// Optional set of macros that should not be passed to the function, even though they are contained. + /// This can be used together with `data_storage_name` to dynamically pass arguments to the function. + block_pass_macros: Option>, +} + +impl Group { + /// Create a new group of commands. + #[must_use] + pub fn new(commands: Vec) -> Self { + Self { + commands, + always_create_function: false, + data_storage_name: None, + block_pass_macros: None, + } + } + + #[must_use] + pub fn always_create_function(mut self, always_create_function: bool) -> Self { + self.always_create_function = always_create_function; + self + } + + #[must_use] + pub fn block_pass_macros(mut self, block: HashSet) -> Self { + self.block_pass_macros = Some(block); + self + } + + #[must_use] + pub fn data_storage_name(mut self, name: String) -> Self { + self.data_storage_name = Some(name); + self + } + + /// Compile the execute command into a list of compiled commands. + #[expect(clippy::too_many_lines)] + #[tracing::instrument(skip_all, fields(commands = ?self.commands))] + pub fn compile( + &self, + options: &CompileOptions, + global_state: &MutCompilerState, + function_state: &FunctionCompilerState, + ) -> Vec { + let command_count = self + .commands + .iter() + .map(|cmd| cmd.get_count(options)) + .sum::(); + // only create a function if there are more than one command + match command_count { + 0 if !self.always_create_function => Vec::new(), + 1 if !self.always_create_function => { + self.commands[0].compile(options, global_state, function_state) + } + _ => { + let pass_macros = self.contains_macro(); + let contains_return = self.contains_return(); + + // calculate a hashed path for the function in the `sb` subfolder + let function_path = Self::generate_function_path(function_state); + + 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(self.commands.iter().cloned()); + function_state.add_function(&function_path, function); + + let mut function_invocation = format!("function {namespace}:{function_path}"); + + let additional_return_cmds = if contains_return { + let full_path = format!("{namespace}:{function_path}"); + let return_data_path = md5::hash(&full_path).to_hex_lowercase(); + + let pre_cmds = Command::Raw(format!( + "data remove storage shulkerbox:return {return_data_path}" + )) + .compile(options, global_state, function_state) + .into_iter() + .map(|c| c.with_forbid_prefix(true)) + .collect::>(); + let post_condition = Condition::Atom( + format!("data storage shulkerbox:return {return_data_path}").into(), + ); + + let post_cmd_store = global_state + .read() + .unwrap() + .functions_with_special_return + .get(&format!( + "{}:{}", + function_state.namespace(), + function_state.path() + )) + .cloned().map(|parent_return_data_path| { + Command::Execute(Execute::If( + post_condition.clone(), + Box::new(Execute::Run(Box::new(Command::Raw(format!( + "data modify storage shulkerbox:return {parent_return_data_path} set from storage shulkerbox:return {return_data_path}" + ))))), + None, + )) + }); + + let post_cmd_return = Command::Execute(Execute::If( + post_condition, + Box::new(Execute::Run(Box::new(Command::Raw(format!( + "return run data get storage shulkerbox:return {return_data_path}" + ))))), + None, + )); + + let post_cmds = post_cmd_store + .into_iter() + .chain(std::iter::once(post_cmd_return)) + .flat_map(|cmd| cmd.compile(options, global_state, function_state)) + .map(|c| c.with_forbid_prefix(true)) + .collect::>(); + + global_state + .write() + .unwrap() + .functions_with_special_return + .insert(full_path, return_data_path); + + Some((pre_cmds, post_cmds)) + } else { + None + }; + + let prepare_data_storage = if pass_macros { + let contained_macros = self.get_macros(); + let not_all_macros_blocked = self + .block_pass_macros + .as_ref() + .is_none_or(|b| contained_macros.iter().any(|&m| !b.contains(m))); + + if !contained_macros.is_empty() + && (self.data_storage_name.is_some() || not_all_macros_blocked) + { + use std::fmt::Write as _; + + // 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 = self + .get_macros() + .into_iter() + .filter(|&m| { + self.block_pass_macros + .as_ref() + .is_none_or(|b| !b.contains(m)) + }) + .map(|m| format!(r#"{m}:"$({m})""#)) + .collect::>() + .join(","); + + if let Some(data_storage_name) = self.data_storage_name.as_deref() { + let _ = + write!(function_invocation, " with storage {data_storage_name}"); + + not_all_macros_blocked.then(|| { + CompiledCommand::new(format!( + "data merge storage {data_storage_name} {{{macros_block}}}" + )) + .with_contains_macros(true) + }) + } else { + let _ = write!(function_invocation, " {{{macros_block}}}"); + + None + } + } else { + None + } + } else { + None + }; + + if let Some((mut pre_cmds, post_cmds)) = additional_return_cmds { + if let Some(prepare_datastorage_cmd) = prepare_data_storage { + pre_cmds.push(prepare_datastorage_cmd); + } + pre_cmds.push( + CompiledCommand::new(function_invocation) + .with_contains_macros(pass_macros && self.data_storage_name.is_none()), + ); + pre_cmds.extend(post_cmds); + pre_cmds + } else { + prepare_data_storage + .into_iter() + .chain(std::iter::once( + CompiledCommand::new(function_invocation).with_contains_macros( + pass_macros && self.data_storage_name.is_none(), + ), + )) + .collect() + } + } + } + } + + /// Check whether the group contains a macro. + pub fn contains_macro(&self) -> bool { + self.commands.iter().any(Command::contains_macro) + } + + /// Check whether the group contains a return command. + pub fn contains_return(&self) -> bool { + self.commands.iter().any(Command::contains_return) + } + + /// Generate a unique function path based on the function state. + fn generate_function_path(function_state: &FunctionCompilerState) -> String { + let uid = function_state.request_uid(); + 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] + } + + /// Returns the names of the macros used + #[must_use] + pub fn get_macros(&self) -> HashSet<&str> { + let mut macros = HashSet::new(); + for cmd in &self.commands { + macros.extend(cmd.get_macros()); + } + macros + } + + /// Check whether the group should not have a prefix. + #[must_use] + pub fn forbid_prefix(&self) -> bool { + self.commands.len() == 1 && self.commands[0].forbid_prefix() + } + + /// Get the count of the commands this command will compile into. + #[must_use] + fn get_count(&self, options: &CompileOptions) -> usize { + let command_count = self + .commands + .iter() + .map(|cmd| cmd.get_count(options)) + .sum::(); + // only create a function if there are more than one command + match command_count { + 0 if !self.always_create_function => 0, + 1 if !self.always_create_function => 1, + _ => { + let pass_macros = self.contains_macro(); + let contains_return = self.contains_return(); + + let additional_return_cmds = if contains_return { + let post_cmd_store = Command::Execute(Execute::If( + Condition::Atom("".into()), + Box::new(Execute::Run(Box::new(Command::Raw(String::new())))), + None, + )) + .get_count(options); + + let post_cmd_return = Command::Execute(Execute::If( + Condition::Atom("".into()), + Box::new(Execute::Run(Box::new(Command::Raw(String::new())))), + None, + )) + .get_count(options); + + let post_cmds = post_cmd_store + post_cmd_return; + + Some(post_cmds + 1) + } else { + None + }; + + let prepare_data_storage = if pass_macros { + let contained_macros = self.get_macros(); + let not_all_macros_blocked = self + .block_pass_macros + .as_ref() + .is_none_or(|b| contained_macros.iter().any(|&m| !b.contains(m))); + + !contained_macros.is_empty() + && (self.data_storage_name.is_some() || not_all_macros_blocked) + & self.data_storage_name.is_some() + } else { + false + }; + + additional_return_cmds.map_or_else( + || 1 + usize::from(prepare_data_storage), + |additional_return_cmds| { + additional_return_cmds + 1 + usize::from(prepare_data_storage) + }, + ) + } + } + } +} + +impl Hash for Group { + fn hash(&self, state: &mut H) { + self.commands.hash(state); + self.always_create_function.hash(state); + if let Some(block) = &self.block_pass_macros { + #[expect(clippy::collection_is_never_read)] + let mut block_vec = block.iter().collect::>(); + block_vec.sort(); + block_vec.hash(state); + } + } +} + /// Represents a command that returns a value. #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Debug, Clone, PartialEq, Eq, Hash)] @@ -232,144 +563,6 @@ fn compile_debug(message: &MacroString, option: &CompileOptions) -> Vec 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 pass_macros = group_contains_macro(commands); - let contains_return = commands.iter().any(Command::contains_return); - - // calculate a hashed path for the function in the `sb` subfolder - let function_path = generate_group_function_path(function_state); - - 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}"); - - let additional_return_cmds = if contains_return { - let full_path = format!("{namespace}:{function_path}"); - let return_data_path = md5::hash(&full_path).to_hex_lowercase(); - - let pre_cmds = Command::Raw(format!( - "data remove storage shulkerbox:return {return_data_path}" - )) - .compile(options, global_state, function_state) - .into_iter() - .map(|c| c.with_forbid_prefix(true)) - .collect::>(); - let post_condition = Condition::Atom( - format!("data storage shulkerbox:return {return_data_path}").into(), - ); - - let post_cmd_store = global_state - .read() - .unwrap() - .functions_with_special_return - .get(&format!( - "{}:{}", - function_state.namespace(), - function_state.path() - )) - .cloned().map(|parent_return_data_path| { - Command::Execute(Execute::If( - post_condition.clone(), - Box::new(Execute::Run(Box::new(Command::Raw(format!( - "data modify storage shulkerbox:return {parent_return_data_path} set from storage shulkerbox:return {return_data_path}" - ))))), - None, - )) - }); - - let post_cmd_return = Command::Execute(Execute::If( - post_condition, - Box::new(Execute::Run(Box::new(Command::Raw(format!( - "return run data get storage shulkerbox:return {return_data_path}" - ))))), - None, - )); - - let post_cmds = post_cmd_store - .into_iter() - .chain(std::iter::once(post_cmd_return)) - .flat_map(|cmd| cmd.compile(options, global_state, function_state)) - .map(|c| c.with_forbid_prefix(true)) - .collect::>(); - - global_state - .write() - .unwrap() - .functions_with_special_return - .insert(full_path, return_data_path); - - Some((pre_cmds, post_cmds)) - } else { - None - }; - - 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}}}")); - } - - if let Some((mut pre_cmds, post_cmds)) = additional_return_cmds { - pre_cmds.push( - CompiledCommand::new(function_invocation).with_contains_macros(pass_macros), - ); - pre_cmds.extend(post_cmds); - pre_cmds - } else { - vec![CompiledCommand::new(function_invocation).with_contains_macros(pass_macros)] - } - } - } -} - -fn generate_group_function_path(function_state: &FunctionCompilerState) -> String { - let uid = function_state.request_uid(); - 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] -} - -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 -} - impl ReturnCommand { pub fn compile( &self, @@ -400,12 +593,12 @@ impl ReturnCommand { vec![store_cmd, return_cmd] } (Self::Command(cmd), None) => { - let compiled_cmd = Command::Group(vec![*cmd.clone()]).compile( + let compiled_cmd = Command::Group(Group::new(vec![*cmd.clone()])).compile( options, global_state, function_state, ); - let compiled_cmd = compiled_cmd + let compiled_cmd = dbg!(compiled_cmd) .into_iter() .next() .expect("group will always return exactly one command"); @@ -414,7 +607,9 @@ impl ReturnCommand { (Self::Command(cmd), Some(data_path)) => { let compiled_cmd = Command::Execute(Execute::Store( format!("result storage shulkerbox:return {data_path} int 1.0").into(), - Box::new(Execute::Run(Box::new(Command::Group(vec![*cmd.clone()])))), + Box::new(Execute::Run(Box::new(Command::Group(Group::new(vec![ + *cmd.clone(), + ]))))), )) .compile(options, global_state, function_state); let compiled_cmd = compiled_cmd diff --git a/src/datapack/mod.rs b/src/datapack/mod.rs index 2b68316..a5e1412 100644 --- a/src/datapack/mod.rs +++ b/src/datapack/mod.rs @@ -4,7 +4,7 @@ mod command; mod function; mod namespace; pub mod tag; -pub use command::{Command, Condition, Execute, ReturnCommand}; +pub use command::{Command, Condition, Execute, Group, ReturnCommand}; pub use function::Function; pub use namespace::Namespace; @@ -32,7 +32,7 @@ pub struct Datapack { } impl Datapack { - pub const LATEST_FORMAT: u8 = 61; + pub const LATEST_FORMAT: u8 = 81; /// Create a new Minecraft datapack. #[must_use]