Compare commits

...

8 Commits

Author SHA1 Message Date
Moritz Hölting 46499b6abe fix example
- "$(" is still invalid when in the same line as a macro usage, as it currently cannot be escaped in minecraft
2025-02-26 09:12:15 +01:00
Moritz Hölting 76d58c0766 fix underflow error in get_count 2024-11-11 17:13:20 +01:00
Moritz Hölting 588bc3f464 only prefix commands with '$' that actually use macros 2024-11-10 21:42:53 +01:00
Moritz Hölting 8f05fef703 fix using other execute variants after if resulting in `run execute` 2024-11-10 15:42:57 +01:00
Moritz Hölting 897e85c2d7 add macro string to execute command 2024-11-08 00:32:52 +01:00
Moritz Hölting 72f98849ea convert debug command to use macro string 2024-11-07 14:40:25 +01:00
Moritz Hölting 2c9fa613ed pass on macros to group 2024-11-06 14:10:02 +01:00
Moritz Hölting 84ee2a5295 implement basic macro string 2024-11-06 13:18:27 +01:00
9 changed files with 817 additions and 197 deletions

View File

@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [Unreleased]
### Added ### Added
- support for commands using macros
### Changed ### Changed
- use "return" command for conditionals instead of data storage when using supported pack format - use "return" command for conditionals instead of data storage when using supported pack format

View File

@ -22,7 +22,7 @@ fn main() {
bar_function.add_command(call_func); bar_function.add_command(call_func);
// add a complex command to the function "bar" // add a complex command to the function "bar"
bar_function.add_command(Command::Execute(Execute::As( bar_function.add_command(Command::Execute(Execute::As(
"@a".to_string(), "@a".into(),
Box::new(Execute::If( Box::new(Execute::If(
Condition::from("block ~ ~ ~ minecraft:stone") Condition::from("block ~ ~ ~ minecraft:stone")
| !(!Condition::from("block ~ ~1 ~ minecraft:stone") | !(!Condition::from("block ~ ~1 ~ minecraft:stone")

View File

@ -1,31 +1,40 @@
use chksum_md5 as md5; use chksum_md5 as md5;
use std::ops::{BitAnd, BitOr, Not}; use std::{
collections::HashSet,
ops::{BitAnd, BitOr, Not},
};
use crate::{ use crate::{
prelude::Command, prelude::Command,
util::compile::{CompileOptions, FunctionCompilerState, MutCompilerState}, util::{
compile::{CompileOptions, CompiledCommand, FunctionCompilerState, MutCompilerState},
MacroString,
},
}; };
use super::Execute; use super::Execute;
/// Compile an if condition command. /// Compile an if condition command.
/// The first tuple element is a boolean indicating if the prefix should be used for that command. /// The first tuple element is a boolean indicating if the prefix should be used for that command.
#[expect(clippy::too_many_arguments)]
#[tracing::instrument(skip_all)] #[tracing::instrument(skip_all)]
pub fn compile_if_cond( pub fn compile_if_cond(
cond: &Condition, cond: &Condition,
then: &Execute, then: &Execute,
el: Option<&Execute>, el: Option<&Execute>,
prefix: &str, prefix: &str,
prefix_contains_macros: bool,
options: &CompileOptions, options: &CompileOptions,
global_state: &MutCompilerState, global_state: &MutCompilerState,
function_state: &FunctionCompilerState, function_state: &FunctionCompilerState,
) -> Vec<(bool, String)> { ) -> Vec<CompiledCommand> {
if options.pack_format < 20 { if options.pack_format < 20 {
compile_pre_20_format( compile_pre_20_format(
cond, cond,
then, then,
el, el,
prefix, prefix,
prefix_contains_macros,
options, options,
global_state, global_state,
function_state, function_state,
@ -36,6 +45,7 @@ pub fn compile_if_cond(
then, then,
el, el,
prefix, prefix,
prefix_contains_macros,
options, options,
global_state, global_state,
function_state, function_state,
@ -43,16 +53,18 @@ pub fn compile_if_cond(
} }
} }
#[allow(clippy::too_many_lines)] #[expect(clippy::too_many_lines, clippy::too_many_arguments)]
fn compile_pre_20_format( fn compile_pre_20_format(
cond: &Condition, cond: &Condition,
then: &Execute, then: &Execute,
el: Option<&Execute>, el: Option<&Execute>,
prefix: &str, prefix: &str,
prefix_contains_macros: bool,
options: &CompileOptions, options: &CompileOptions,
global_state: &MutCompilerState, global_state: &MutCompilerState,
function_state: &FunctionCompilerState, function_state: &FunctionCompilerState,
) -> Vec<(bool, String)> { ) -> Vec<CompiledCommand> {
let contains_macro = prefix_contains_macros || cond.contains_macro();
let then_count = then.get_count(options); let then_count = then.get_count(options);
let str_cond = cond.clone().compile(options, global_state, function_state); let str_cond = cond.clone().compile(options, global_state, function_state);
@ -80,13 +92,28 @@ fn compile_pre_20_format(
.into(), .into(),
); );
} }
Command::Group(group_cmd) let group = Command::Group(group_cmd);
let allows_prefix = !group.forbid_prefix();
group
.compile(options, global_state, function_state) .compile(options, global_state, function_state)
.iter() .iter()
.map(|s| (true, "run ".to_string() + s)) .map(|s| {
if allows_prefix {
s.clone().apply_prefix("run ")
} else {
s.clone()
}
})
.collect() .collect()
} else { } else {
then.compile_internal(String::new(), false, options, global_state, function_state) then.compile_internal(
String::new(),
false,
contains_macro,
options,
global_state,
function_state,
)
}; };
// if the conditions have multiple parts joined by a disjunction, commands need to be grouped // if the conditions have multiple parts joined by a disjunction, commands need to be grouped
let each_or_cmd = (str_cond.len() > 1).then(|| { let each_or_cmd = (str_cond.len() > 1).then(|| {
@ -98,10 +125,10 @@ fn compile_pre_20_format(
format!("data modify storage shulkerbox:cond {success_uid} set value true"), format!("data modify storage shulkerbox:cond {success_uid} set value true"),
combine_conditions_commands( combine_conditions_commands(
str_cond.clone(), str_cond.clone(),
&[( &[CompiledCommand::new(format!(
true, "run data modify storage shulkerbox:cond {success_uid} set value true"
format!("run data modify storage shulkerbox:cond {success_uid} set value true"), ))
)], .or_contains_macros(contains_macro)],
), ),
) )
}); });
@ -111,11 +138,10 @@ fn compile_pre_20_format(
tracing::error!("No success_uid found for each_or_cmd, using default"); tracing::error!("No success_uid found for each_or_cmd, using default");
"if_success" "if_success"
}); });
Condition::Atom(format!("data storage shulkerbox:cond {{{success_uid}:1b}}")).compile( Condition::Atom(MacroString::from(format!(
options, "data storage shulkerbox:cond {{{success_uid}:1b}}"
global_state, )))
function_state, .compile(options, global_state, function_state)
)
} else { } else {
str_cond str_cond
}; };
@ -128,12 +154,14 @@ fn compile_pre_20_format(
tracing::error!("No success_uid found for each_or_cmd, using default"); tracing::error!("No success_uid found for each_or_cmd, using default");
"if_success" "if_success"
}); });
let else_cond = let else_cond = (!Condition::Atom(MacroString::from(format!(
(!Condition::Atom(format!("data storage shulkerbox:cond {{{success_uid}:1b}}"))) "data storage shulkerbox:cond {{{success_uid}:1b}}"
.compile(options, global_state, function_state); ))))
.compile(options, global_state, function_state);
let el = el.compile_internal( let el = el.compile_internal(
String::new(), String::new(),
else_cond.len() > 1, else_cond.len() > 1,
contains_macro,
options, options,
global_state, global_state,
function_state, function_state,
@ -148,10 +176,10 @@ fn compile_pre_20_format(
tracing::error!("No success_uid found for each_or_cmd, using default"); tracing::error!("No success_uid found for each_or_cmd, using default");
"if_success" "if_success"
}); });
Some(( Some(
false, CompiledCommand::new(format!("data remove storage shulkerbox:cond {success_uid}"))
format!("data remove storage shulkerbox:cond {success_uid}"), .with_forbid_prefix(true),
)) )
} else { } else {
None None
}; };
@ -164,29 +192,27 @@ fn compile_pre_20_format(
.chain(then_commands) .chain(then_commands)
.chain(el_commands) .chain(el_commands)
.chain(reset_success_storage) .chain(reset_success_storage)
.map(|(use_prefix, cmd)| { .map(|cmd| cmd.apply_prefix(prefix))
let cmd = if use_prefix {
prefix.to_string() + &cmd
} else {
cmd
};
(use_prefix, cmd)
})
.collect() .collect()
} }
#[expect(clippy::too_many_arguments)]
fn compile_since_20_format( fn compile_since_20_format(
cond: &Condition, cond: &Condition,
then: &Execute, then: &Execute,
el: Option<&Execute>, el: Option<&Execute>,
prefix: &str, prefix: &str,
prefix_contains_macros: bool,
options: &CompileOptions, options: &CompileOptions,
global_state: &MutCompilerState, global_state: &MutCompilerState,
function_state: &FunctionCompilerState, function_state: &FunctionCompilerState,
) -> Vec<(bool, String)> { ) -> Vec<CompiledCommand> {
let contains_macros = prefix_contains_macros || cond.contains_macro();
let then_count = then.get_count(options); let then_count = then.get_count(options);
let str_cond = cond.clone().compile(options, global_state, function_state); let str_cond = cond
.clone()
.compile_keep_macros(options, global_state, function_state);
// if the conditions have multiple parts joined by a disjunction or an else part, commands need to be grouped // if the conditions have multiple parts joined by a disjunction or an else part, commands need to be grouped
if el.is_some() || str_cond.len() > 1 { if el.is_some() || str_cond.len() > 1 {
@ -199,45 +225,56 @@ fn compile_since_20_format(
global_state, global_state,
function_state, function_state,
); );
Command::Group(group_cmds) let group = Command::Group(group_cmds);
.compile(options, global_state, function_state) let cmds = group.compile(options, global_state, function_state);
.into_iter() if contains_macros {
.map(|s| (true, s)) cmds.into_iter()
.collect() .map(|cmd| cmd.or_contains_macros(true))
.collect()
} else {
cmds
}
} else if then_count > 1 { } else if then_count > 1 {
let then_cmd = match then.clone() { let then_cmd = 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)],
}; };
let then_cmd_str = Command::Group(then_cmd) let group_cmd = Command::Group(then_cmd);
.compile(options, global_state, function_state) let then_cmd = if group_cmd.forbid_prefix() {
group_cmd
} else {
Command::Concat(
Box::new(Command::Raw("run ".to_string())),
Box::new(group_cmd),
)
};
combine_conditions_commands_concat(str_cond, &then_cmd)
.into_iter() .into_iter()
.map(|s| (true, format!("run {s}"))) .flat_map(|cmd| {
.collect::<Vec<_>>(); cmd.compile(options, global_state, function_state)
combine_conditions_commands(str_cond, &then_cmd_str) .into_iter()
.into_iter() .map(move |compiled_cmd| {
.map(|(use_prefix, cmd)| { compiled_cmd
let cmd = if use_prefix { .apply_prefix(prefix)
prefix.to_string() + &cmd .or_contains_macros(contains_macros)
} else { })
cmd
};
(use_prefix, cmd)
}) })
.collect() .collect()
} else { } else {
let str_cmd = str_cond
then.compile_internal(String::new(), false, options, global_state, function_state);
combine_conditions_commands(str_cond, &str_cmd)
.into_iter() .into_iter()
.map(|(use_prefix, cmd)| { .flat_map(|cond| {
let cmd = if use_prefix { then.compile_internal(
prefix.to_string() + &cmd String::new(),
} else { false,
cmd contains_macros,
}; options,
(use_prefix, cmd) global_state,
function_state,
)
.into_iter()
.map(move |cmd| cmd.apply_prefix(prefix.to_string() + &cond.compile() + " "))
}) })
.collect() .collect()
} }
@ -245,26 +282,43 @@ fn compile_since_20_format(
fn combine_conditions_commands( fn combine_conditions_commands(
conditions: Vec<String>, conditions: Vec<String>,
commands: &[(bool, String)], commands: &[CompiledCommand],
) -> Vec<(bool, String)> { ) -> Vec<CompiledCommand> {
conditions conditions
.into_iter() .into_iter()
.flat_map(|cond| { .flat_map(|cond| {
commands.iter().map(move |(use_prefix, cmd)| { let prefix = cond + " ";
commands.iter().map(move |cmd| {
// combine the condition with the command if it uses a prefix // combine the condition with the command if it uses a prefix
let cmd = if *use_prefix { cmd.clone().apply_prefix(&prefix)
cond.clone() + " " + cmd
} else {
cmd.clone()
};
(*use_prefix, cmd)
}) })
}) })
.collect() .collect()
} }
fn combine_conditions_commands_concat(
conditions: Vec<MacroString>,
command: &Command,
) -> Vec<Command> {
if command.forbid_prefix() {
vec![command.clone()]
} else {
conditions
.into_iter()
.map(|cond| {
let prefix = if cond.contains_macro() {
Command::UsesMacro(cond + " ")
} else {
Command::Raw(cond.compile() + " ")
};
Command::Concat(Box::new(prefix), Box::new(command.clone()))
})
.collect()
}
}
fn handle_return_group_case_since_20( fn handle_return_group_case_since_20(
str_cond: Vec<String>, str_cond: Vec<MacroString>,
then: &Execute, then: &Execute,
el: Option<&Execute>, el: Option<&Execute>,
prefix: &str, prefix: &str,
@ -278,15 +332,28 @@ 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 then_cmd_str = Command::Group(then_cmd) let group = Command::Group(then_cmd);
.compile(options, global_state, function_state) let then_cmd_concat = if group.forbid_prefix() {
group
} else {
Command::Concat(
Box::new(Command::Raw("run return run ".to_string())),
Box::new(group),
)
};
let then_cond_concat = combine_conditions_commands_concat(str_cond, &then_cmd_concat);
let mut group_cmds = then_cond_concat
.into_iter() .into_iter()
.map(|s| (true, format!("run return run {s}"))) .map(|cmd| {
.collect::<Vec<_>>(); if cmd.forbid_prefix() {
let then_cond_str = combine_conditions_commands(str_cond, &then_cmd_str); cmd
let mut group_cmds = then_cond_str } else {
.into_iter() Command::Concat(
.map(|(_, cmd)| Command::Raw(format!("execute {cmd}"))) Box::new(Command::Raw("execute ".to_string())),
Box::new(cmd),
)
}
})
.collect::<Vec<_>>(); .collect::<Vec<_>>();
if let Some(el) = el { if let Some(el) = el {
handle_else_since_20( handle_else_since_20(
@ -311,7 +378,7 @@ fn handle_else_since_20(
) { ) {
let el_cmd = match el { let el_cmd = match el {
Execute::If(cond, then, el) => handle_return_group_case_since_20( Execute::If(cond, then, el) => handle_return_group_case_since_20(
cond.compile(options, global_state, function_state), cond.compile_keep_macros(options, global_state, function_state),
&then, &then,
el.as_deref(), el.as_deref(),
prefix, prefix,
@ -321,7 +388,7 @@ fn handle_else_since_20(
), ),
Execute::Run(cmd) => match *cmd { Execute::Run(cmd) => match *cmd {
Command::Execute(Execute::If(cond, then, el)) => handle_return_group_case_since_20( Command::Execute(Execute::If(cond, then, el)) => handle_return_group_case_since_20(
cond.compile(options, global_state, function_state), cond.compile_keep_macros(options, global_state, function_state),
&then, &then,
el.as_deref(), el.as_deref(),
prefix, prefix,
@ -343,7 +410,7 @@ fn handle_else_since_20(
#[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, Hash)]
pub enum Condition { pub enum Condition {
Atom(String), Atom(MacroString),
Not(Box<Condition>), Not(Box<Condition>),
And(Box<Condition>, Box<Condition>), And(Box<Condition>, Box<Condition>),
Or(Box<Condition>, Box<Condition>), Or(Box<Condition>, Box<Condition>),
@ -392,21 +459,21 @@ impl Condition {
} }
} }
/// Convert the condition into a string. /// Convert the condition into a [`MacroString`].
/// ///
/// Will fail if the condition contains an `Or` variant. Use `compile` instead. /// Will fail if the condition contains an `Or` variant. Use `compile` instead.
fn str_cond(&self) -> Option<String> { fn str_cond(&self) -> Option<MacroString> {
match self { match self {
Self::Atom(s) => Some("if ".to_string() + s), Self::Atom(s) => Some(MacroString::from("if ") + s.clone()),
Self::Not(n) => match *(*n).clone() { Self::Not(n) => match *(*n).clone() {
Self::Atom(s) => Some("unless ".to_string() + &s), Self::Atom(s) => Some(MacroString::from("unless ") + s),
_ => None, _ => None,
}, },
Self::And(a, b) => { Self::And(a, b) => {
let a = a.str_cond()?; let a = a.str_cond()?;
let b = b.str_cond()?; let b = b.str_cond()?;
Some(a + " " + &b) Some(a + MacroString::from(" ") + b)
} }
Self::Or(..) => None, Self::Or(..) => None,
} }
@ -414,11 +481,24 @@ impl Condition {
/// Compile the condition into a list of strings that can be used in Minecraft. /// Compile the condition into a list of strings that can be used in Minecraft.
pub fn compile( pub fn compile(
&self,
options: &CompileOptions,
global_state: &MutCompilerState,
function_state: &FunctionCompilerState,
) -> Vec<String> {
self.compile_keep_macros(options, global_state, function_state)
.into_iter()
.map(|s| s.compile())
.collect()
}
/// Compile the condition into a list of macro strings.
pub fn compile_keep_macros(
&self, &self,
_options: &CompileOptions, _options: &CompileOptions,
_global_state: &MutCompilerState, _global_state: &MutCompilerState,
_function_state: &FunctionCompilerState, _function_state: &FunctionCompilerState,
) -> Vec<String> { ) -> Vec<MacroString> {
let truth_table = self.to_truth_table(); let truth_table = self.to_truth_table();
truth_table truth_table
@ -427,13 +507,37 @@ impl Condition {
c.str_cond() c.str_cond()
.expect("Truth table should not contain Or variants") .expect("Truth table should not contain Or variants")
}) })
.collect() .collect::<Vec<_>>()
}
/// Check whether the condition contains a macro.
#[must_use]
pub fn contains_macro(&self) -> bool {
match self {
Self::Atom(s) => s.contains_macro(),
Self::Not(n) => n.contains_macro(),
Self::And(a, b) | Self::Or(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::Atom(s) => s.get_macros(),
Self::Not(n) => n.get_macros(),
Self::And(a, b) | Self::Or(a, b) => {
let mut set = a.get_macros();
set.extend(b.get_macros());
set
}
}
} }
} }
impl From<&str> for Condition { impl From<&str> for Condition {
fn from(s: &str) -> Self { fn from(s: &str) -> Self {
Self::Atom(s.to_string()) Self::Atom(s.into())
} }
} }
@ -459,12 +563,6 @@ impl BitOr for Condition {
} }
} }
impl From<Execute> for Command {
fn from(ex: Execute) -> Self {
Self::Execute(ex)
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
@ -473,8 +571,8 @@ mod tests {
#[test] #[test]
fn test_condition() { fn test_condition() {
let c1 = Condition::from("foo"); let c1 = Condition::from("foo");
let c2 = Condition::Atom("bar".to_string()); let c2 = Condition::Atom("bar".into());
let c3 = Condition::Atom("baz".to_string()); let c3 = Condition::Atom("baz".into());
assert_eq!( assert_eq!(
(c1.clone() & c2.clone()).normalize(), (c1.clone() & c2.clone()).normalize(),
@ -517,10 +615,10 @@ mod tests {
#[allow(clippy::redundant_clone)] #[allow(clippy::redundant_clone)]
#[test] #[test]
fn test_truth_table() { fn test_truth_table() {
let c1 = Condition::Atom("foo".to_string()); let c1 = Condition::Atom("foo".into());
let c2 = Condition::Atom("bar".to_string()); let c2 = Condition::Atom("bar".into());
let c3 = Condition::Atom("baz".to_string()); let c3 = Condition::Atom("baz".into());
let c4 = Condition::Atom("foobar".to_string()); let c4 = Condition::Atom("foobar".into());
assert_eq!( assert_eq!(
(c1.clone() & c2.clone()).to_truth_table(), (c1.clone() & c2.clone()).to_truth_table(),
@ -562,18 +660,21 @@ mod tests {
.into_iter() .into_iter()
.map(str::to_string) .map(str::to_string)
.collect(); .collect();
let commands = &[(true, "1".to_string()), (false, "2".to_string())]; let commands = &[
CompiledCommand::new("1".to_string()),
CompiledCommand::new("2".to_string()).with_forbid_prefix(true),
];
let combined = combine_conditions_commands(conditions, commands); let combined = combine_conditions_commands(conditions, commands);
assert_eq!( assert_eq!(
combined, combined,
vec![ vec![
(true, "a 1".to_string()), CompiledCommand::new("a 1".to_string()),
(false, "2".to_string()), CompiledCommand::new("2".to_string()).with_forbid_prefix(true),
(true, "b 1".to_string()), CompiledCommand::new("b 1".to_string()),
(false, "2".to_string()), CompiledCommand::new("2".to_string()).with_forbid_prefix(true),
(true, "c 1".to_string()), CompiledCommand::new("c 1".to_string()),
(false, "2".to_string()) CompiledCommand::new("2".to_string()).with_forbid_prefix(true)
] ]
); );
} }

View File

@ -1,9 +1,9 @@
use std::ops::RangeInclusive; use std::{collections::HashSet, ops::RangeInclusive, string::ToString};
use super::Command; use super::Command;
use crate::util::{ use crate::util::{
compile::{CompileOptions, FunctionCompilerState, MutCompilerState}, compile::{CompileOptions, CompiledCommand, FunctionCompilerState, MutCompilerState},
ExtendableQueue, ExtendableQueue, MacroString,
}; };
mod conditional; mod conditional;
@ -15,18 +15,18 @@ pub use conditional::Condition;
#[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, Hash)]
pub enum Execute { pub enum Execute {
Align(String, Box<Execute>), Align(MacroString, Box<Execute>),
Anchored(String, Box<Execute>), Anchored(MacroString, Box<Execute>),
As(String, Box<Execute>), As(MacroString, Box<Execute>),
At(String, Box<Execute>), At(MacroString, Box<Execute>),
AsAt(String, Box<Execute>), AsAt(MacroString, Box<Execute>),
Facing(String, Box<Execute>), Facing(MacroString, Box<Execute>),
In(String, Box<Execute>), In(MacroString, Box<Execute>),
On(String, Box<Execute>), On(MacroString, Box<Execute>),
Positioned(String, Box<Execute>), Positioned(MacroString, Box<Execute>),
Rotated(String, Box<Execute>), Rotated(MacroString, Box<Execute>),
Store(String, Box<Execute>), Store(MacroString, Box<Execute>),
Summon(String, Box<Execute>), Summon(MacroString, Box<Execute>),
If(Condition, Box<Execute>, Option<Box<Execute>>), If(Condition, Box<Execute>, Option<Box<Execute>>),
Run(Box<Command>), Run(Box<Command>),
Runs(Vec<Command>), Runs(Vec<Command>),
@ -39,35 +39,38 @@ impl Execute {
options: &CompileOptions, options: &CompileOptions,
global_state: &MutCompilerState, global_state: &MutCompilerState,
function_state: &FunctionCompilerState, function_state: &FunctionCompilerState,
) -> Vec<String> { ) -> Vec<CompiledCommand> {
// Directly compile the command if it is a run command, skipping the execute part // Directly compile the command if it is a run command, skipping the execute part
// Otherwise, compile the execute command using internal function // Otherwise, compile the execute command using internal function
if let Self::Run(cmd) = self { match self {
cmd.compile(options, global_state, function_state) Self::Run(cmd) => cmd.compile(options, global_state, function_state),
} else { Self::Runs(cmds) => cmds
self.compile_internal( .iter()
.flat_map(|c| c.compile(options, global_state, function_state))
.collect(),
_ => self.compile_internal(
String::from("execute "), String::from("execute "),
false, false,
false,
options, options,
global_state, global_state,
function_state, function_state,
) ),
.into_iter()
.map(|(_, cmd)| cmd)
.collect()
} }
} }
/// Compile the execute command into strings with the given prefix. /// Compile the execute command into strings with the given prefix.
/// Each first tuple element is a boolean indicating if the prefix should be used for that command. /// Each first tuple element is a boolean indicating if the prefix should be used for that command.
#[expect(clippy::too_many_lines)]
fn compile_internal( fn compile_internal(
&self, &self,
prefix: String, prefix: String,
require_grouping: bool, require_grouping: bool,
prefix_contains_macros: bool,
options: &CompileOptions, options: &CompileOptions,
global_state: &MutCompilerState, global_state: &MutCompilerState,
function_state: &FunctionCompilerState, function_state: &FunctionCompilerState,
) -> Vec<(bool, String)> { ) -> Vec<CompiledCommand> {
match self { match self {
Self::Align(arg, next) Self::Align(arg, next)
| Self::Anchored(arg, next) | Self::Anchored(arg, next)
@ -79,15 +82,24 @@ impl Execute {
| Self::Positioned(arg, next) | Self::Positioned(arg, next)
| Self::Rotated(arg, next) | Self::Rotated(arg, next)
| Self::Store(arg, next) => next.compile_internal( | Self::Store(arg, next) => next.compile_internal(
format!("{prefix}{op} {arg} ", op = self.variant_name()), format!(
"{prefix}{op} {arg} ",
op = self.variant_name(),
arg = arg.compile()
),
require_grouping, require_grouping,
prefix_contains_macros || arg.contains_macro(),
options, options,
global_state, global_state,
function_state, function_state,
), ),
Self::AsAt(selector, next) => next.compile_internal( Self::AsAt(selector, next) => next.compile_internal(
format!("{prefix}as {selector} at @s "), format!(
"{prefix}as {selector} at @s ",
selector = selector.compile()
),
require_grouping, require_grouping,
prefix_contains_macros || selector.contains_macro(),
options, options,
global_state, global_state,
function_state, function_state,
@ -97,21 +109,28 @@ impl Execute {
then.as_ref(), then.as_ref(),
el.as_deref(), el.as_deref(),
&prefix, &prefix,
prefix_contains_macros,
options, options,
global_state, global_state,
function_state, function_state,
), ),
Self::Summon(arg, next) => next.compile_internal( Self::Summon(arg, next) => next.compile_internal(
format!("{prefix}{op} {arg} ", op = self.variant_name()), format!(
"{prefix}{op} {arg} ",
op = self.variant_name(),
arg = arg.compile()
),
true, true,
prefix_contains_macros || arg.contains_macro(),
options, options,
global_state, global_state,
function_state, function_state,
), ),
Self::Run(command) => match &**command { Self::Run(command) => match command.as_ref() {
Command::Execute(ex) => ex.compile_internal( Command::Execute(ex) => ex.compile_internal(
prefix, prefix,
require_grouping, require_grouping,
prefix_contains_macros,
options, options,
global_state, global_state,
function_state, function_state,
@ -119,19 +138,40 @@ impl Execute {
command => command command => command
.compile(options, global_state, function_state) .compile(options, global_state, function_state)
.into_iter() .into_iter()
.map(|c| map_run_cmd(c, &prefix)) .map(|c| {
map_run_cmd(command.forbid_prefix(), c, &prefix)
.or_contains_macros(prefix_contains_macros)
})
.collect(), .collect(),
}, },
Self::Runs(commands) if !require_grouping => commands Self::Runs(commands) if !require_grouping => commands
.iter() .iter()
.flat_map(|c| c.compile(options, global_state, function_state)) .flat_map(|c| match c {
.map(|c| map_run_cmd(c, &prefix)) Command::Execute(ex) => ex.compile_internal(
.collect(), prefix.clone(),
Self::Runs(commands) => Command::Group(commands.clone()) require_grouping,
.compile(options, global_state, function_state) prefix_contains_macros,
.into_iter() options,
.map(|c| map_run_cmd(c, &prefix)) global_state,
function_state,
),
command => command.compile(options, global_state, function_state),
})
.map(|cmd| {
map_run_cmd(false, cmd, &prefix).or_contains_macros(prefix_contains_macros)
})
.collect(), .collect(),
Self::Runs(commands) => {
let group = Command::Group(commands.clone());
group
.compile(options, global_state, function_state)
.into_iter()
.map(|c| {
map_run_cmd(group.forbid_prefix(), c, &prefix)
.or_contains_macros(prefix_contains_macros)
})
.collect()
}
} }
} }
@ -145,6 +185,7 @@ impl Execute {
self.compile_internal( self.compile_internal(
String::new(), String::new(),
false, false,
false,
options, options,
&global_state, &global_state,
&function_state, &function_state,
@ -200,16 +241,90 @@ impl Execute {
} }
} }
} }
/// Check whether the execute command contains a macro.
#[must_use]
pub fn contains_macro(&self) -> bool {
match self {
Self::Facing(s, next)
| Self::Store(s, next)
| Self::Positioned(s, next)
| Self::Rotated(s, next)
| Self::In(s, next)
| Self::As(s, next)
| Self::At(s, next)
| Self::AsAt(s, next)
| Self::Align(s, next)
| Self::Anchored(s, next)
| Self::Summon(s, next)
| Self::On(s, next) => s.contains_macro() || next.contains_macro(),
Self::If(cond, then, el) => {
cond.contains_macro()
|| then.contains_macro()
|| el.as_deref().map_or(false, Self::contains_macro)
}
Self::Run(cmd) => cmd.contains_macro(),
Self::Runs(cmds) => cmds.iter().any(super::Command::contains_macro),
}
}
/// Returns the names of the macros used
#[must_use]
pub fn get_macros(&self) -> HashSet<&str> {
match self {
Self::Facing(s, _)
| Self::Store(s, _)
| Self::Positioned(s, _)
| Self::Rotated(s, _)
| Self::In(s, _)
| Self::As(s, _)
| Self::At(s, _)
| Self::AsAt(s, _)
| Self::Align(s, _)
| Self::Anchored(s, _)
| Self::Summon(s, _)
| Self::On(s, _) => s.get_macros(),
Self::If(cond, then, el) => {
let mut macros = cond.get_macros();
macros.extend(then.get_macros());
if let Some(el) = el {
macros.extend(el.get_macros());
}
macros
}
Self::Run(cmd) => cmd.get_macros(),
Self::Runs(cmds) => cmds.iter().flat_map(|cmd| cmd.get_macros()).collect(),
}
}
}
impl From<Execute> for Command {
fn from(execute: Execute) -> Self {
Self::Execute(execute)
}
}
impl From<Command> for Execute {
fn from(command: Command) -> Self {
Self::Run(Box::new(command))
}
}
impl<V> From<V> for Execute
where
V: Into<Vec<Command>>,
{
fn from(value: V) -> Self {
Self::Runs(value.into())
}
} }
/// Combine command parts, respecting if the second part is a comment /// Combine command parts, respecting if the second part is a comment
/// The first tuple element is a boolean indicating if the prefix should be used /// The first tuple element is a boolean indicating if the prefix should be used
fn map_run_cmd(cmd: String, prefix: &str) -> (bool, String) { fn map_run_cmd(forbid_prefix: bool, cmd: CompiledCommand, prefix: &str) -> CompiledCommand {
if cmd.starts_with('#') || cmd.is_empty() || cmd.chars().all(char::is_whitespace) { let forbid_prefix =
(false, cmd) forbid_prefix || cmd.as_str().is_empty() || cmd.as_str().chars().all(char::is_whitespace);
} else { cmd.or_forbid_prefix(forbid_prefix)
(true, prefix.to_string() + "run " + &cmd) .apply_prefix(prefix.to_string() + "run ")
}
} }
#[cfg(test)] #[cfg(test)]
@ -219,7 +334,7 @@ mod tests {
#[test] #[test]
fn test_compile() { fn test_compile() {
let compiled = Execute::As( let compiled = Execute::As(
"@ְa".to_string(), "@a".into(),
Box::new(Execute::If( Box::new(Execute::If(
"block ~ ~-1 ~ minecraft:stone".into(), "block ~ ~-1 ~ minecraft:stone".into(),
Box::new(Execute::Run(Box::new("say hi".into()))), Box::new(Execute::Run(Box::new("say hi".into()))),
@ -234,7 +349,9 @@ mod tests {
assert_eq!( assert_eq!(
compiled, compiled,
vec!["execute as @ְa if block ~ ~-1 ~ minecraft:stone run say hi".to_string()] vec![CompiledCommand::new(
"execute as @a if block ~ ~-1 ~ minecraft:stone run say hi"
)]
); );
let direct = Execute::Run(Box::new("say direct".into())).compile( let direct = Execute::Run(Box::new("say direct".into())).compile(
@ -243,6 +360,6 @@ mod tests {
&FunctionCompilerState::default(), &FunctionCompilerState::default(),
); );
assert_eq!(direct, vec!["say direct".to_string()]); assert_eq!(direct, vec![CompiledCommand::new("say direct")]);
} }
} }

View File

@ -1,7 +1,11 @@
//! Represents a command that can be included in a function. //! Represents a command that can be included in a function.
mod execute; mod execute;
use std::{collections::HashMap, ops::RangeInclusive, sync::OnceLock}; use std::{
collections::{HashMap, HashSet},
ops::RangeInclusive,
sync::OnceLock,
};
pub use execute::{Condition, Execute}; pub use execute::{Condition, Execute};
@ -10,7 +14,10 @@ use chksum_md5 as md5;
use super::Function; use super::Function;
use crate::{ use crate::{
prelude::Datapack, prelude::Datapack,
util::compile::{CompileOptions, FunctionCompilerState, MutCompilerState}, util::{
compile::{CompileOptions, CompiledCommand, FunctionCompilerState, MutCompilerState},
MacroString,
},
}; };
/// Represents a command that can be included in a function. /// Represents a command that can be included in a function.
@ -19,14 +26,18 @@ use crate::{
pub enum Command { pub enum Command {
/// A command that is already formatted as a string. /// A command that is already formatted as a string.
Raw(String), Raw(String),
/// A command that contains macro usages
UsesMacro(MacroString),
/// Message to be printed only in debug mode /// Message to be printed only in debug mode
Debug(String), Debug(MacroString),
/// 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(Vec<Command>),
/// Comment to be added to the function /// Comment to be added to the function
Comment(String), Comment(String),
/// Command that is a concatenation of two commands
Concat(Box<Command>, Box<Command>),
} }
impl Command { impl Command {
@ -42,13 +53,37 @@ impl Command {
options: &CompileOptions, options: &CompileOptions,
global_state: &MutCompilerState, global_state: &MutCompilerState,
function_state: &FunctionCompilerState, function_state: &FunctionCompilerState,
) -> Vec<String> { ) -> Vec<CompiledCommand> {
match self { match self {
Self::Raw(command) => vec![command.clone()], 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::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(commands) => compile_group(commands, options, global_state, function_state),
Self::Comment(comment) => vec!["#".to_string() + comment], Self::Comment(comment) => {
vec![CompiledCommand::new("#".to_string() + comment).with_forbid_prefix(true)]
}
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()
}
} }
} }
@ -60,18 +95,66 @@ impl Command {
Self::Comment(_) => 0, Self::Comment(_) => 0,
Self::Debug(_) => usize::from(options.debug), Self::Debug(_) => usize::from(options.debug),
Self::Raw(cmd) => cmd.split('\n').count(), Self::Raw(cmd) => cmd.split('\n').count(),
Self::UsesMacro(cmd) => cmd.line_count(),
Self::Execute(ex) => ex.get_count(options), Self::Execute(ex) => ex.get_count(options),
Self::Group(_) => 1, Self::Group(_) => 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. /// Check whether the command is valid with the given pack format.
#[must_use] #[must_use]
pub fn validate(&self, pack_formats: &RangeInclusive<u8>) -> bool { pub fn validate(&self, pack_formats: &RangeInclusive<u8>) -> bool {
match self { let command_valid = match self {
Self::Comment(_) | Self::Debug(_) | Self::Group(_) => true, Self::Comment(_) | Self::Debug(_) | Self::Group(_) => true,
Self::Raw(cmd) => validate_raw_cmd(cmd, pack_formats), 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::Execute(ex) => ex.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::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::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::Concat(a, _) => a.forbid_prefix(),
} }
} }
} }
@ -92,12 +175,12 @@ impl From<&mut Function> for Command {
} }
} }
fn compile_debug(message: &str, option: &CompileOptions) -> Vec<String> { fn compile_debug(message: &MacroString, option: &CompileOptions) -> Vec<CompiledCommand> {
if option.debug { if option.debug {
vec![format!( 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 message to 'false' to disable"}}]}}}},{{"text":"]","color":"dark_blue"}},{{"text":" {}","color":"black"}}]"#, 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 message.compile()
)] ))]
} else { } else {
Vec::new() Vec::new()
} }
@ -109,42 +192,67 @@ fn compile_group(
options: &CompileOptions, options: &CompileOptions,
global_state: &MutCompilerState, global_state: &MutCompilerState,
function_state: &FunctionCompilerState, function_state: &FunctionCompilerState,
) -> Vec<String> { ) -> Vec<CompiledCommand> {
let command_count = commands let command_count = 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
if command_count > 1 { match command_count {
let uid = function_state.request_uid(); 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 // calculate a hashed path for the function in the `sb` subfolder
let function_path = { let function_path = {
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);
let pre_hash_path = function_path.to_owned() + ":" + &uid.to_string(); let pre_hash_path = function_path.to_owned() + ":" + &uid.to_string();
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]
}; };
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(commands.iter().cloned());
function_state.add_function(&function_path, function); function_state.add_function(&function_path, function);
vec![format!("function {namespace}:{function_path}")] let mut function_invocation = format!("function {namespace}:{function_path}");
} else {
commands if pass_macros {
.iter() // WARNING: this seems to be the only way to pass macros to the function called.
.flat_map(|cmd| cmd.compile(options, global_state, function_state)) // Because everything is passed as a string, it looses one "level" of escaping per pass.
.collect::<Vec<_>>() 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)] #[allow(clippy::too_many_lines)]
fn validate_raw_cmd(cmd: &str, pack_formats: &RangeInclusive<u8>) -> bool { fn validate_raw_cmd(cmd: &str, pack_formats: &RangeInclusive<u8>) -> bool {
static CMD_FORMATS: OnceLock<HashMap<&str, RangeInclusive<u8>>> = OnceLock::new(); static CMD_FORMATS: OnceLock<HashMap<&str, RangeInclusive<u8>>> = OnceLock::new();
@ -287,12 +395,12 @@ mod tests {
assert_eq!( assert_eq!(
command_a.compile(options, global_state, function_state), command_a.compile(options, global_state, function_state),
vec!["say Hello, world!".to_string()] vec![CompiledCommand::new("say Hello, world!")]
); );
assert_eq!(command_a.get_count(options), 1); assert_eq!(command_a.get_count(options), 1);
assert_eq!( assert_eq!(
command_b.compile(options, global_state, function_state), command_b.compile(options, global_state, function_state),
vec!["say foo bar".to_string()] vec![CompiledCommand::new("say foo bar")]
); );
assert_eq!(command_b.get_count(options), 1); assert_eq!(command_b.get_count(options), 1);
} }
@ -307,7 +415,7 @@ mod tests {
assert_eq!( assert_eq!(
comment.compile(options, global_state, function_state), comment.compile(options, global_state, function_state),
vec!["#this is a comment".to_string()] vec![CompiledCommand::new("#this is a comment").with_forbid_prefix(true)]
); );
assert_eq!(comment.get_count(options), 0); assert_eq!(comment.get_count(options), 0);
} }

View File

@ -62,8 +62,26 @@ impl Function {
let content = self let content = self
.commands .commands
.iter() .iter()
.flat_map(|c| c.compile(options, global_state, function_state)) .flat_map(|c| {
.collect::<Vec<String>>() let cmds = c.compile(options, global_state, function_state);
if c.contains_macro() {
cmds.into_iter()
.map(|c| {
if c.contains_macros() {
let content = format!("${c}");
c.with_command(content)
} else {
c
}
.to_string()
})
.collect::<Vec<_>>()
} else {
cmds.into_iter().map(|c| c.to_string()).collect::<Vec<_>>()
}
})
.collect::<Vec<_>>()
.join("\n"); .join("\n");
VFile::Text(content) VFile::Text(content)
} }

View File

@ -1,6 +1,6 @@
//! Compile options for the compiler. //! Compile options for the compiler.
use std::sync::Mutex; use std::{fmt::Display, ops::Deref, sync::Mutex};
use getset::Getters; use getset::Getters;
@ -86,3 +86,127 @@ impl FunctionCompilerState {
uid uid
} }
} }
/// Compiled command, ready to be written to a function.
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, Default, PartialEq, Eq, Hash)]
pub struct CompiledCommand {
/// The command string.
command: String,
/// Whether the command is not allowed to be prefixed.
forbid_prefix: bool,
/// Whether the command contains a macro.
contains_macros: bool,
}
impl CompiledCommand {
/// Create a new compiled command.
#[must_use]
pub fn new<S>(command: S) -> Self
where
S: Into<String>,
{
Self {
command: command.into(),
forbid_prefix: false,
contains_macros: false,
}
}
/// Get the command string.
#[must_use]
pub fn as_str(&self) -> &str {
&self.command
}
/// Set the command string.
#[must_use]
pub fn with_command(mut self, command: String) -> Self {
self.command = command;
self
}
/// Set whether the command is forbidden to be prefixed.
#[must_use]
pub fn with_forbid_prefix(mut self, forbid_prefix: bool) -> Self {
self.forbid_prefix = forbid_prefix;
self
}
/// Set whether the command contains a macro.
#[must_use]
pub fn with_contains_macros(mut self, contains_macros: bool) -> Self {
self.contains_macros = contains_macros;
self
}
/// Get whether the command is forbidden to be prefixed.
#[must_use]
pub fn forbids_prefix(&self) -> bool {
self.forbid_prefix
}
/// Get whether the command contains a macro.
#[must_use]
pub fn contains_macros(&self) -> bool {
self.contains_macros
}
/// Apply a prefix to the command (if allowed).
#[must_use]
pub fn apply_prefix<S>(mut self, prefix: S) -> Self
where
S: Into<String>,
{
if !self.forbid_prefix {
self.command = prefix.into() + &self.command;
}
self
}
/// Combine current forbid prefix status with the input.
#[must_use]
pub fn or_forbid_prefix(mut self, forbid_prefix: bool) -> Self {
self.forbid_prefix = self.forbid_prefix || forbid_prefix;
self
}
/// Combine current contains macro status with the input.
#[must_use]
pub fn or_contains_macros(mut self, contains_macros: bool) -> Self {
self.contains_macros = self.contains_macros || contains_macros;
self
}
}
impl Deref for CompiledCommand {
type Target = String;
fn deref(&self) -> &Self::Target {
&self.command
}
}
impl Display for CompiledCommand {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.command)
}
}
impl From<CompiledCommand> for String {
fn from(compiled_command: CompiledCommand) -> Self {
compiled_command.command
}
}
impl From<String> for CompiledCommand {
fn from(value: String) -> Self {
Self::new(value)
}
}
impl From<&str> for CompiledCommand {
fn from(value: &str) -> Self {
Self::new(value.to_string())
}
}

147
src/util/macro_string.rs Normal file
View File

@ -0,0 +1,147 @@
#![allow(clippy::module_name_repetitions)]
use std::{borrow::Cow, collections::HashSet, ops::Add};
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum MacroString {
String(String),
MacroString(Vec<MacroStringPart>),
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum MacroStringPart {
String(String),
MacroUsage(String),
}
impl MacroString {
/// Returns whether the [`MacroString`] contains any macro usages
#[must_use]
pub fn contains_macro(&self) -> bool {
match self {
Self::String(_) => false,
Self::MacroString(parts) => !parts
.iter()
.all(|p| matches!(p, MacroStringPart::String(_))),
}
}
/// Compiles to a string that Minecraft can interpret
#[must_use]
pub fn compile(&self) -> String {
match self {
Self::String(s) => s.to_owned(),
Self::MacroString(parts) => parts
.iter()
.map(|p| match p {
MacroStringPart::String(s) => Cow::Borrowed(s),
MacroStringPart::MacroUsage(m) => Cow::Owned(format!("$({m})")),
})
.collect::<Vec<_>>()
.iter()
.map(|p| p.as_str())
.collect::<Vec<_>>()
.join(""),
}
}
/// Returns the amount of lines the string has
#[must_use]
pub fn line_count(&self) -> usize {
match self {
Self::String(s) => s.split('\n').count(),
Self::MacroString(parts) => {
parts
.iter()
.map(|p| match p {
MacroStringPart::String(s) => s.split('\n').count() - 1,
MacroStringPart::MacroUsage(_) => 0,
})
.sum::<usize>()
+ 1
}
}
}
/// Returns the names of the macros used in the [`MacroString`]
#[must_use]
pub fn get_macros(&self) -> HashSet<&str> {
match self {
Self::String(_) => HashSet::new(),
Self::MacroString(parts) => parts
.iter()
.filter_map(|p| match p {
MacroStringPart::String(_) => None,
MacroStringPart::MacroUsage(m) => Some(m.as_str()),
})
.collect(),
}
}
}
impl From<String> for MacroString {
fn from(value: String) -> Self {
Self::String(value)
}
}
impl From<&str> for MacroString {
fn from(value: &str) -> Self {
Self::String(value.to_string())
}
}
impl From<String> for MacroStringPart {
fn from(value: String) -> Self {
Self::String(value)
}
}
impl From<&str> for MacroStringPart {
fn from(value: &str) -> Self {
Self::String(value.to_string())
}
}
impl Add for MacroString {
type Output = Self;
fn add(self, rhs: Self) -> Self::Output {
match (self, rhs) {
(Self::String(mut s1), Self::String(s2)) => {
s1.push_str(&s2);
Self::String(s1)
}
(Self::String(s1), Self::MacroString(s2)) => Self::MacroString(
std::iter::once(MacroStringPart::String(s1))
.chain(s2)
.collect(),
),
(Self::MacroString(mut s1), Self::String(s2)) => {
s1.push(MacroStringPart::String(s2));
Self::MacroString(s1)
}
(Self::MacroString(mut s1), Self::MacroString(s2)) => {
s1.extend(s2);
Self::MacroString(s1)
}
}
}
}
impl Add<&str> for MacroString {
type Output = Self;
fn add(self, rhs: &str) -> Self::Output {
match self {
Self::String(mut s1) => {
s1.push_str(rhs);
Self::String(s1)
}
Self::MacroString(mut s1) => {
s1.push(MacroStringPart::String(rhs.to_string()));
Self::MacroString(s1)
}
}
}
}

View File

@ -2,7 +2,11 @@
pub mod compile; pub mod compile;
mod extendable_queue; mod extendable_queue;
mod macro_string;
pub(crate) mod pack_format; pub(crate) mod pack_format;
#[doc(inline)] #[doc(inline)]
pub use extendable_queue::ExtendableQueue; pub use extendable_queue::ExtendableQueue;
#[doc(inline)]
pub use macro_string::{MacroString, MacroStringPart};