add group options for always extracting and manually loading macros

This commit is contained in:
Moritz Hölting 2025-09-02 18:00:26 +02:00
parent b6ecdf6385
commit 89709834da
4 changed files with 359 additions and 160 deletions

View File

@ -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 {

View File

@ -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()

View File

@ -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<Command>),
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<Command>,
/// 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<String>,
/// 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<HashSet<String>>,
}
impl Group {
/// Create a new group of commands.
#[must_use]
pub fn new(commands: Vec<Command>) -> 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<String>) -> 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<CompiledCommand> {
let command_count = self
.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 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::<Vec<_>>();
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::<Vec<_>>();
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::<Vec<_>>()
.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::<usize>();
// 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<H: std::hash::Hasher>(&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::<Vec<_>>();
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<Compiled
}
}
#[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 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::<Vec<_>>();
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::<Vec<_>>();
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::<Vec<_>>()
.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

View File

@ -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]