Compare commits

..

2 Commits

Author SHA1 Message Date
Moritz Hölting d4689c696a add getters to group command 2025-09-02 22:46:27 +02:00
Moritz Hölting 89709834da add group options for always extracting and manually loading macros 2025-09-02 18:00:26 +02:00
4 changed files with 391 additions and 159 deletions

View File

@ -5,6 +5,7 @@ use std::{
}; };
use crate::{ use crate::{
datapack::command::Group,
prelude::Command, prelude::Command,
util::{ util::{
compile::{CompileOptions, CompiledCommand, FunctionCompilerState, MutCompilerState}, compile::{CompileOptions, CompiledCommand, FunctionCompilerState, MutCompilerState},
@ -78,7 +79,7 @@ fn compile_using_data_storage(
#[allow(clippy::option_if_let_else)] #[allow(clippy::option_if_let_else)]
let then = if let Some(success_uid) = require_grouping_uid.as_deref() { let then = if let Some(success_uid) = require_grouping_uid.as_deref() {
// prepare commands for grouping // prepare commands for grouping
let mut group_cmd = match then.clone() { let mut group_cmds = match then.clone() {
Execute::Run(cmd) => vec![*cmd], Execute::Run(cmd) => vec![*cmd],
Execute::Runs(cmds) => cmds, Execute::Runs(cmds) => cmds,
ex => vec![Command::Execute(ex)], ex => vec![Command::Execute(ex)],
@ -86,13 +87,13 @@ fn compile_using_data_storage(
// add success condition to the group // add success condition to the group
// this condition will be checked after the group ran to determine if the else part should be executed // 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 { 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") format!("data modify storage shulkerbox:cond {success_uid} set value true")
.as_str() .as_str()
.into(), .into(),
); );
} }
let group = Command::Group(group_cmd); let group = Command::Group(Group::new(group_cmds));
let allows_prefix = !group.forbid_prefix(); let allows_prefix = !group.forbid_prefix();
group group
.compile(options, global_state, function_state) .compile(options, global_state, function_state)
@ -227,7 +228,7 @@ fn compile_since_20_format(
global_state, global_state,
function_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); let cmds = group.compile(options, global_state, function_state);
if contains_macros { if contains_macros {
cmds.into_iter() cmds.into_iter()
@ -254,7 +255,7 @@ fn compile_since_20_format(
Execute::Runs(cmds) => cmds, Execute::Runs(cmds) => cmds,
ex => vec![Command::Execute(ex)], 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() { let then_cmd = if group_cmd.forbid_prefix() {
group_cmd group_cmd
} else { } else {
@ -346,7 +347,7 @@ fn handle_return_group_case_since_20(
Execute::Runs(cmds) => cmds, Execute::Runs(cmds) => cmds,
ex => vec![Command::Execute(ex)], 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() { let then_cmd_concat = if group.forbid_prefix() {
group group
} else { } else {

View File

@ -1,9 +1,12 @@
use std::{collections::HashSet, ops::RangeInclusive, string::ToString}; use std::{collections::HashSet, ops::RangeInclusive, string::ToString};
use super::Command; use super::Command;
use crate::util::{ use crate::{
datapack::command::Group,
util::{
compile::{CompileOptions, CompiledCommand, FunctionCompilerState, MutCompilerState}, compile::{CompileOptions, CompiledCommand, FunctionCompilerState, MutCompilerState},
ExtendableQueue, MacroString, ExtendableQueue, MacroString,
},
}; };
mod conditional; mod conditional;
@ -162,7 +165,7 @@ impl Execute {
}) })
.collect(), .collect(),
Self::Runs(commands) => { Self::Runs(commands) => {
let group = Command::Group(commands.clone()); let group = Command::Group(Group::new(commands.clone()));
group group
.compile(options, global_state, function_state) .compile(options, global_state, function_state)
.into_iter() .into_iter()

View File

@ -3,6 +3,7 @@
mod execute; mod execute;
use std::{ use std::{
collections::{HashMap, HashSet}, collections::{HashMap, HashSet},
hash::Hash,
ops::RangeInclusive, ops::RangeInclusive,
sync::LazyLock, sync::LazyLock,
}; };
@ -33,7 +34,7 @@ pub enum Command {
/// Execute command /// Execute command
Execute(Execute), Execute(Execute),
/// Group of commands to be called instantly after each other /// Group of commands to be called instantly after each other
Group(Vec<Command>), Group(Group),
/// Comment to be added to the function /// Comment to be added to the function
Comment(String), Comment(String),
/// Return value /// Return value
@ -64,7 +65,7 @@ impl Command {
} }
Self::Debug(message) => compile_debug(message, options), Self::Debug(message) => compile_debug(message, options),
Self::Execute(ex) => ex.compile(options, global_state, function_state), 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) => { Self::Comment(comment) => {
vec![CompiledCommand::new("#".to_string() + comment).with_forbid_prefix(true)] vec![CompiledCommand::new("#".to_string() + comment).with_forbid_prefix(true)]
} }
@ -101,7 +102,8 @@ impl Command {
Self::Raw(cmd) => cmd.split('\n').count(), Self::Raw(cmd) => cmd.split('\n').count(),
Self::UsesMacro(cmd) => cmd.line_count(), Self::UsesMacro(cmd) => cmd.line_count(),
Self::Execute(ex) => ex.get_count(options), 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, Self::Concat(a, b) => a.get_count(options) + b.get_count(options) - 1,
} }
} }
@ -135,7 +137,7 @@ impl Command {
match self { match self {
Self::Raw(_) | Self::Comment(_) => false, Self::Raw(_) | Self::Comment(_) => false,
Self::UsesMacro(s) | Self::Debug(s) => s.contains_macro(), 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::Execute(ex) => ex.contains_macro(),
Self::Return(ret) => match ret { Self::Return(ret) => match ret {
ReturnCommand::Value(value) => value.contains_macro(), ReturnCommand::Value(value) => value.contains_macro(),
@ -151,7 +153,7 @@ impl Command {
match self { match self {
Self::Raw(_) | Self::Comment(_) => HashSet::new(), Self::Raw(_) | Self::Comment(_) => HashSet::new(),
Self::UsesMacro(s) | Self::Debug(s) => s.get_macros(), 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::Execute(ex) => ex.get_macros(),
Self::Return(ret) => match ret { Self::Return(ret) => match ret {
ReturnCommand::Value(value) => value.get_macros(), ReturnCommand::Value(value) => value.get_macros(),
@ -171,7 +173,7 @@ impl Command {
match self { match self {
Self::Comment(_) => true, Self::Comment(_) => true,
Self::Raw(_) | Self::Debug(_) | Self::Execute(_) | Self::UsesMacro(_) => false, 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 { Self::Return(ret) => match ret {
ReturnCommand::Value(_) => false, ReturnCommand::Value(_) => false,
ReturnCommand::Command(cmd) => cmd.forbid_prefix(), ReturnCommand::Command(cmd) => cmd.forbid_prefix(),
@ -190,7 +192,7 @@ impl Command {
Self::Execute(exec) => exec.contains_return(), Self::Execute(exec) => exec.contains_return(),
Self::Raw(cmd) => cmd.starts_with("return "), Self::Raw(cmd) => cmd.starts_with("return "),
Self::UsesMacro(m) => m.compile().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,54 +213,85 @@ impl From<&mut Function> for Command {
} }
} }
/// Represents a command that returns a value. /// Represents a group of commands to be executed in sequence.
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq, Hash)] #[derive(Debug, Clone, PartialEq, Eq)]
pub enum ReturnCommand { pub struct Group {
/// Returns the value /// The commands in the group.
Value(MacroString), commands: Vec<Command>,
/// Returns the result of the command /// Whether to always create a function for this group, even if it contains only one command.
Command(Box<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>>,
} }
fn compile_debug(message: &MacroString, option: &CompileOptions) -> Vec<CompiledCommand> { impl Group {
if option.debug { /// Create a new group of commands.
vec![CompiledCommand::new(format!( #[must_use]
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"}}]"#, pub fn new(commands: Vec<Command>) -> Self {
message.compile() Self {
))] commands,
} else { always_create_function: false,
Vec::new() data_storage_name: None,
block_pass_macros: None,
}
} }
}
#[tracing::instrument(skip_all, fields(commands = ?commands))] #[must_use]
fn compile_group( pub fn with_always_create_function(mut self, always_create_function: bool) -> Self {
commands: &[Command], self.always_create_function = always_create_function;
self
}
#[must_use]
pub fn with_block_pass_macros(mut self, block: HashSet<String>) -> Self {
self.block_pass_macros = Some(block);
self
}
#[must_use]
pub fn with_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, options: &CompileOptions,
global_state: &MutCompilerState, global_state: &MutCompilerState,
function_state: &FunctionCompilerState, function_state: &FunctionCompilerState,
) -> Vec<CompiledCommand> { ) -> Vec<CompiledCommand> {
let command_count = commands let command_count = self
.commands
.iter() .iter()
.map(|cmd| cmd.get_count(options)) .map(|cmd| cmd.get_count(options))
.sum::<usize>(); .sum::<usize>();
// only create a function if there are more than one command // only create a function if there are more than one command
match command_count { match command_count {
0 => Vec::new(), 0 if !self.always_create_function => Vec::new(),
1 => commands[0].compile(options, global_state, function_state), 1 if !self.always_create_function => {
self.commands[0].compile(options, global_state, function_state)
}
_ => { _ => {
let pass_macros = group_contains_macro(commands); let pass_macros = self.contains_macro();
let contains_return = commands.iter().any(Command::contains_return); let contains_return = self.contains_return();
// calculate a hashed path for the function in the `sb` subfolder // calculate a hashed path for the function in the `sb` subfolder
let function_path = generate_group_function_path(function_state); let function_path = Self::generate_function_path(function_state);
let namespace = function_state.namespace(); let namespace = function_state.namespace();
// create a new function with the commands // create a new function with the commands
let mut function = Function::new(namespace, &function_path); let mut function = Function::new(namespace, &function_path);
function.get_commands_mut().extend(commands.iter().cloned()); function
.get_commands_mut()
.extend(self.commands.iter().cloned());
function_state.add_function(&function_path, function); function_state.add_function(&function_path, function);
let mut function_invocation = format!("function {namespace}:{function_path}"); let mut function_invocation = format!("function {namespace}:{function_path}");
@ -323,31 +356,123 @@ fn compile_group(
None None
}; };
if pass_macros { 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. // 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. // Because everything is passed as a string, it looses one "level" of escaping per pass.
let macros_block = group_get_macros(commands) let macros_block = self
.get_macros()
.into_iter() .into_iter()
.filter(|&m| {
self.block_pass_macros
.as_ref()
.is_none_or(|b| !b.contains(m))
})
.map(|m| format!(r#"{m}:"$({m})""#)) .map(|m| format!(r#"{m}:"$({m})""#))
.collect::<Vec<_>>() .collect::<Vec<_>>()
.join(","); .join(",");
function_invocation.push_str(&format!(" {{{macros_block}}}"));
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((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( pre_cmds.push(
CompiledCommand::new(function_invocation).with_contains_macros(pass_macros), CompiledCommand::new(function_invocation)
.with_contains_macros(pass_macros && self.data_storage_name.is_none()),
); );
pre_cmds.extend(post_cmds); pre_cmds.extend(post_cmds);
pre_cmds pre_cmds
} else { } else {
vec![CompiledCommand::new(function_invocation).with_contains_macros(pass_macros)] 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()
}
} }
} }
} }
}
fn generate_group_function_path(function_state: &FunctionCompilerState) -> String { /// Get a reference to the commands in the group.
#[must_use]
pub fn commands(&self) -> &[Command] {
&self.commands
}
/// Get a mutable reference to the commands in the group.
#[must_use]
pub fn commands_mut(&mut self) -> &mut Vec<Command> {
&mut self.commands
}
/// Get whether to always create a function for this group.
#[must_use]
pub fn always_create_function(&self) -> bool {
self.always_create_function
}
/// Get the optional data storage name.
#[must_use]
pub fn data_storage_name(&self) -> Option<&String> {
self.data_storage_name.as_ref()
}
/// Get the optional set of blocked macros.
#[must_use]
pub fn block_pass_macros(&self) -> Option<&HashSet<String>> {
self.block_pass_macros.as_ref()
}
/// Check whether the group contains a macro.
#[must_use]
pub fn contains_macro(&self) -> bool {
self.commands.iter().any(Command::contains_macro)
}
/// Check whether the group contains a return command.
#[must_use]
pub fn contains_return(&self) -> bool {
self.commands.iter().any(Command::contains_return)
}
/// Generate a unique function path based on the function state.
#[must_use]
fn generate_function_path(function_state: &FunctionCompilerState) -> String {
let uid = function_state.request_uid(); let uid = function_state.request_uid();
let function_path = function_state.path(); let function_path = function_state.path();
let function_path = function_path.strip_prefix("sb/").unwrap_or(function_path); let function_path = function_path.strip_prefix("sb/").unwrap_or(function_path);
@ -356,18 +481,119 @@ fn generate_group_function_path(function_state: &FunctionCompilerState) -> Strin
let hash = md5::hash(pre_hash_path).to_hex_lowercase(); let hash = md5::hash(pre_hash_path).to_hex_lowercase();
"sb/".to_string() + function_path + "/" + &hash[..16] "sb/".to_string() + function_path + "/" + &hash[..16]
} }
fn group_contains_macro(commands: &[Command]) -> bool { /// Returns the names of the macros used
commands.iter().any(Command::contains_macro) #[must_use]
} pub fn get_macros(&self) -> HashSet<&str> {
fn group_get_macros(commands: &[Command]) -> HashSet<&str> {
let mut macros = HashSet::new(); let mut macros = HashSet::new();
for cmd in commands { for cmd in &self.commands {
macros.extend(cmd.get_macros()); macros.extend(cmd.get_macros());
} }
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)]
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()
}
} }
impl ReturnCommand { impl ReturnCommand {
@ -400,7 +626,7 @@ impl ReturnCommand {
vec![store_cmd, return_cmd] vec![store_cmd, return_cmd]
} }
(Self::Command(cmd), None) => { (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, options,
global_state, global_state,
function_state, function_state,
@ -414,7 +640,9 @@ impl ReturnCommand {
(Self::Command(cmd), Some(data_path)) => { (Self::Command(cmd), Some(data_path)) => {
let compiled_cmd = Command::Execute(Execute::Store( let compiled_cmd = Command::Execute(Execute::Store(
format!("result storage shulkerbox:return {data_path} int 1.0").into(), 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); .compile(options, global_state, function_state);
let compiled_cmd = compiled_cmd let compiled_cmd = compiled_cmd

View File

@ -4,7 +4,7 @@ mod command;
mod function; mod function;
mod namespace; mod namespace;
pub mod tag; pub mod tag;
pub use command::{Command, Condition, Execute, ReturnCommand}; pub use command::{Command, Condition, Execute, Group, ReturnCommand};
pub use function::Function; pub use function::Function;
pub use namespace::Namespace; pub use namespace::Namespace;
@ -32,7 +32,7 @@ pub struct Datapack {
} }
impl Datapack { impl Datapack {
pub const LATEST_FORMAT: u8 = 61; pub const LATEST_FORMAT: u8 = 81;
/// Create a new Minecraft datapack. /// Create a new Minecraft datapack.
#[must_use] #[must_use]