Compare commits

..

No commits in common. "588bc3f46434b9a06deb2030f4fe0c1829a98a59" and "897e85c2d73576c8fadb442df3b91446dea12a20" have entirely different histories.

6 changed files with 156 additions and 329 deletions

View File

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

View File

@ -7,7 +7,7 @@ use std::{
use crate::{
prelude::Command,
util::{
compile::{CompileOptions, CompiledCommand, FunctionCompilerState, MutCompilerState},
compile::{CompileOptions, FunctionCompilerState, MutCompilerState},
MacroString,
},
};
@ -16,25 +16,22 @@ use super::Execute;
/// Compile an if condition 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)]
pub fn compile_if_cond(
cond: &Condition,
then: &Execute,
el: Option<&Execute>,
prefix: &str,
prefix_contains_macros: bool,
options: &CompileOptions,
global_state: &MutCompilerState,
function_state: &FunctionCompilerState,
) -> Vec<CompiledCommand> {
) -> Vec<(bool, String)> {
if options.pack_format < 20 {
compile_pre_20_format(
cond,
then,
el,
prefix,
prefix_contains_macros,
options,
global_state,
function_state,
@ -45,7 +42,6 @@ pub fn compile_if_cond(
then,
el,
prefix,
prefix_contains_macros,
options,
global_state,
function_state,
@ -53,18 +49,16 @@ pub fn compile_if_cond(
}
}
#[expect(clippy::too_many_lines, clippy::too_many_arguments)]
#[allow(clippy::too_many_lines)]
fn compile_pre_20_format(
cond: &Condition,
then: &Execute,
el: Option<&Execute>,
prefix: &str,
prefix_contains_macros: bool,
options: &CompileOptions,
global_state: &MutCompilerState,
function_state: &FunctionCompilerState,
) -> Vec<CompiledCommand> {
let contains_macro = prefix_contains_macros || cond.contains_macro();
) -> Vec<(bool, String)> {
let then_count = then.get_count(options);
let str_cond = cond.clone().compile(options, global_state, function_state);
@ -99,21 +93,14 @@ fn compile_pre_20_format(
.iter()
.map(|s| {
if allows_prefix {
s.clone().apply_prefix("run ")
(true, "run ".to_string() + s)
} else {
s.clone()
(false, s.clone())
}
})
.collect()
} else {
then.compile_internal(
String::new(),
false,
contains_macro,
options,
global_state,
function_state,
)
then.compile_internal(String::new(), false, options, global_state, function_state)
};
// if the conditions have multiple parts joined by a disjunction, commands need to be grouped
let each_or_cmd = (str_cond.len() > 1).then(|| {
@ -125,10 +112,10 @@ fn compile_pre_20_format(
format!("data modify storage shulkerbox:cond {success_uid} set value true"),
combine_conditions_commands(
str_cond.clone(),
&[CompiledCommand::new(format!(
"run data modify storage shulkerbox:cond {success_uid} set value true"
))
.or_contains_macros(contains_macro)],
&[(
true,
format!("run data modify storage shulkerbox:cond {success_uid} set value true"),
)],
),
)
});
@ -161,7 +148,6 @@ fn compile_pre_20_format(
let el = el.compile_internal(
String::new(),
else_cond.len() > 1,
contains_macro,
options,
global_state,
function_state,
@ -176,10 +162,10 @@ fn compile_pre_20_format(
tracing::error!("No success_uid found for each_or_cmd, using default");
"if_success"
});
Some(
CompiledCommand::new(format!("data remove storage shulkerbox:cond {success_uid}"))
.with_forbid_prefix(true),
)
Some((
false,
format!("data remove storage shulkerbox:cond {success_uid}"),
))
} else {
None
};
@ -192,22 +178,26 @@ fn compile_pre_20_format(
.chain(then_commands)
.chain(el_commands)
.chain(reset_success_storage)
.map(|cmd| cmd.apply_prefix(prefix))
.map(|(use_prefix, cmd)| {
let cmd = if use_prefix {
prefix.to_string() + &cmd
} else {
cmd
};
(use_prefix, cmd)
})
.collect()
}
#[expect(clippy::too_many_arguments)]
fn compile_since_20_format(
cond: &Condition,
then: &Execute,
el: Option<&Execute>,
prefix: &str,
prefix_contains_macros: bool,
options: &CompileOptions,
global_state: &MutCompilerState,
function_state: &FunctionCompilerState,
) -> Vec<CompiledCommand> {
let contains_macros = prefix_contains_macros || cond.contains_macro();
) -> Vec<(bool, String)> {
let then_count = then.get_count(options);
let str_cond = cond
@ -226,14 +216,12 @@ fn compile_since_20_format(
function_state,
);
let group = Command::Group(group_cmds);
let cmds = group.compile(options, global_state, function_state);
if contains_macros {
cmds.into_iter()
.map(|cmd| cmd.or_contains_macros(true))
.collect()
} else {
cmds
}
let allows_prefix = !group.forbid_prefix();
group
.compile(options, global_state, function_state)
.into_iter()
.map(|s| (allows_prefix, s))
.collect()
} else if then_count > 1 {
let then_cmd = match then.clone() {
Execute::Run(cmd) => vec![*cmd],
@ -251,46 +239,55 @@ fn compile_since_20_format(
};
combine_conditions_commands_concat(str_cond, &then_cmd)
.into_iter()
.flat_map(|cmd| {
cmd.compile(options, global_state, function_state)
.into_iter()
.map(move |compiled_cmd| {
compiled_cmd
.apply_prefix(prefix)
.or_contains_macros(contains_macros)
})
.map(|cmd| {
(
cmd.forbid_prefix(),
cmd.compile(options, global_state, function_state),
)
})
.flat_map(|(forbid_prefix, cmds)| {
cmds.into_iter()
.map(move |cmd| (!forbid_prefix, prefix.to_string() + &cmd))
})
.collect()
} else {
str_cond
.into_iter()
.flat_map(|cond| {
then.compile_internal(
String::new(),
false,
contains_macros,
options,
global_state,
function_state,
)
.into_iter()
.map(move |cmd| cmd.apply_prefix(prefix.to_string() + &cond.compile() + " "))
})
.collect()
combine_conditions_commands_concat(
str_cond,
&Command::Concat(
Box::new(Command::Raw("run ".to_string())),
Box::new(Command::Execute(then.clone())),
),
)
.into_iter()
.map(|cmd| {
(
cmd.forbid_prefix(),
cmd.compile(options, global_state, function_state),
)
})
.flat_map(|(forbid_prefix, cmds)| {
cmds.into_iter()
.map(move |cmd| (!forbid_prefix, prefix.to_string() + &cmd))
})
.collect()
}
}
fn combine_conditions_commands(
conditions: Vec<String>,
commands: &[CompiledCommand],
) -> Vec<CompiledCommand> {
commands: &[(bool, String)],
) -> Vec<(bool, String)> {
conditions
.into_iter()
.flat_map(|cond| {
let prefix = cond + " ";
commands.iter().map(move |cmd| {
commands.iter().map(move |(use_prefix, cmd)| {
// combine the condition with the command if it uses a prefix
cmd.clone().apply_prefix(&prefix)
let cmd = if *use_prefix {
cond.clone() + " " + cmd
} else {
cmd.clone()
};
(*use_prefix, cmd)
})
})
.collect()
@ -660,21 +657,18 @@ mod tests {
.into_iter()
.map(str::to_string)
.collect();
let commands = &[
CompiledCommand::new("1".to_string()),
CompiledCommand::new("2".to_string()).with_forbid_prefix(true),
];
let commands = &[(true, "1".to_string()), (false, "2".to_string())];
let combined = combine_conditions_commands(conditions, commands);
assert_eq!(
combined,
vec![
CompiledCommand::new("a 1".to_string()),
CompiledCommand::new("2".to_string()).with_forbid_prefix(true),
CompiledCommand::new("b 1".to_string()),
CompiledCommand::new("2".to_string()).with_forbid_prefix(true),
CompiledCommand::new("c 1".to_string()),
CompiledCommand::new("2".to_string()).with_forbid_prefix(true)
(true, "a 1".to_string()),
(false, "2".to_string()),
(true, "b 1".to_string()),
(false, "2".to_string()),
(true, "c 1".to_string()),
(false, "2".to_string())
]
);
}

View File

@ -1,8 +1,8 @@
use std::{collections::HashSet, ops::RangeInclusive, string::ToString};
use std::{collections::HashSet, ops::RangeInclusive};
use super::Command;
use crate::util::{
compile::{CompileOptions, CompiledCommand, FunctionCompilerState, MutCompilerState},
compile::{CompileOptions, FunctionCompilerState, MutCompilerState},
ExtendableQueue, MacroString,
};
@ -39,38 +39,35 @@ impl Execute {
options: &CompileOptions,
global_state: &MutCompilerState,
function_state: &FunctionCompilerState,
) -> Vec<CompiledCommand> {
) -> Vec<String> {
// Directly compile the command if it is a run command, skipping the execute part
// Otherwise, compile the execute command using internal function
match self {
Self::Run(cmd) => cmd.compile(options, global_state, function_state),
Self::Runs(cmds) => cmds
.iter()
.flat_map(|c| c.compile(options, global_state, function_state))
.collect(),
_ => self.compile_internal(
if let Self::Run(cmd) = self {
cmd.compile(options, global_state, function_state)
} else {
self.compile_internal(
String::from("execute "),
false,
false,
options,
global_state,
function_state,
),
)
.into_iter()
.map(|(_, cmd)| cmd)
.collect()
}
}
/// 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.
#[expect(clippy::too_many_lines)]
fn compile_internal(
&self,
prefix: String,
require_grouping: bool,
prefix_contains_macros: bool,
options: &CompileOptions,
global_state: &MutCompilerState,
function_state: &FunctionCompilerState,
) -> Vec<CompiledCommand> {
) -> Vec<(bool, String)> {
match self {
Self::Align(arg, next)
| Self::Anchored(arg, next)
@ -81,14 +78,14 @@ impl Execute {
| Self::On(arg, next)
| Self::Positioned(arg, next)
| Self::Rotated(arg, next)
| Self::Store(arg, next) => next.compile_internal(
| Self::Store(arg, next)
| Self::Summon(arg, next) => next.compile_internal(
format!(
"{prefix}{op} {arg} ",
op = self.variant_name(),
arg = arg.compile()
),
require_grouping,
prefix_contains_macros || arg.contains_macro(),
options,
global_state,
function_state,
@ -99,7 +96,6 @@ impl Execute {
selector = selector.compile()
),
require_grouping,
prefix_contains_macros || selector.contains_macro(),
options,
global_state,
function_state,
@ -109,28 +105,14 @@ impl Execute {
then.as_ref(),
el.as_deref(),
&prefix,
prefix_contains_macros,
options,
global_state,
function_state,
),
Self::Summon(arg, next) => next.compile_internal(
format!(
"{prefix}{op} {arg} ",
op = self.variant_name(),
arg = arg.compile()
),
true,
prefix_contains_macros || arg.contains_macro(),
options,
global_state,
function_state,
),
Self::Run(command) => match command.as_ref() {
Self::Run(command) => match &**command {
Command::Execute(ex) => ex.compile_internal(
prefix,
require_grouping,
prefix_contains_macros,
options,
global_state,
function_state,
@ -138,38 +120,25 @@ impl Execute {
command => command
.compile(options, global_state, function_state)
.into_iter()
.map(|c| {
map_run_cmd(command.forbid_prefix(), c, &prefix)
.or_contains_macros(prefix_contains_macros)
})
.map(|c| map_run_cmd(command.forbid_prefix(), c, &prefix))
.collect(),
},
Self::Runs(commands) if !require_grouping => commands
.iter()
.flat_map(|c| match c {
Command::Execute(ex) => ex.compile_internal(
prefix.clone(),
require_grouping,
prefix_contains_macros,
options,
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)
.flat_map(|c| {
let forbid_prefix = c.forbid_prefix();
c.compile(options, global_state, function_state)
.into_iter()
.map(move |c| (forbid_prefix, c))
})
.map(|(forbid_prefix, c)| map_run_cmd(forbid_prefix, c, &prefix))
.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)
})
.map(|c| map_run_cmd(group.forbid_prefix(), c, &prefix))
.collect()
}
}
@ -185,7 +154,6 @@ impl Execute {
self.compile_internal(
String::new(),
false,
false,
options,
&global_state,
&function_state,
@ -320,11 +288,12 @@ where
/// 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
fn map_run_cmd(forbid_prefix: bool, cmd: CompiledCommand, prefix: &str) -> CompiledCommand {
let forbid_prefix =
forbid_prefix || cmd.as_str().is_empty() || cmd.as_str().chars().all(char::is_whitespace);
cmd.or_forbid_prefix(forbid_prefix)
.apply_prefix(prefix.to_string() + "run ")
fn map_run_cmd(forbid_prefix: bool, cmd: String, prefix: &str) -> (bool, String) {
if forbid_prefix || cmd.is_empty() || cmd.chars().all(char::is_whitespace) {
(false, cmd)
} else {
(true, prefix.to_string() + "run " + &cmd)
}
}
#[cfg(test)]
@ -349,9 +318,7 @@ mod tests {
assert_eq!(
compiled,
vec![CompiledCommand::new(
"execute as @a if block ~ ~-1 ~ minecraft:stone run say hi"
)]
vec!["execute as @a if block ~ ~-1 ~ minecraft:stone run say hi".to_string()]
);
let direct = Execute::Run(Box::new("say direct".into())).compile(
@ -360,6 +327,6 @@ mod tests {
&FunctionCompilerState::default(),
);
assert_eq!(direct, vec![CompiledCommand::new("say direct")]);
assert_eq!(direct, vec!["say direct".to_string()]);
}
}

View File

@ -15,7 +15,7 @@ use super::Function;
use crate::{
prelude::Datapack,
util::{
compile::{CompileOptions, CompiledCommand, FunctionCompilerState, MutCompilerState},
compile::{CompileOptions, FunctionCompilerState, MutCompilerState},
MacroString,
},
};
@ -53,18 +53,14 @@ impl Command {
options: &CompileOptions,
global_state: &MutCompilerState,
function_state: &FunctionCompilerState,
) -> Vec<CompiledCommand> {
) -> Vec<String> {
match self {
Self::Raw(command) => vec![CompiledCommand::new(command.clone())],
Self::UsesMacro(command) => {
vec![CompiledCommand::new(command.compile()).with_contains_macros(true)]
}
Self::Raw(command) => vec![command.clone()],
Self::UsesMacro(command) => vec![command.compile()],
Self::Debug(message) => compile_debug(message, options),
Self::Execute(ex) => ex.compile(options, global_state, function_state),
Self::Group(commands) => compile_group(commands, options, global_state, function_state),
Self::Comment(comment) => {
vec![CompiledCommand::new("#".to_string() + comment).with_forbid_prefix(true)]
}
Self::Comment(comment) => vec!["#".to_string() + comment],
Self::Concat(a, b) => {
let a = a.compile(options, global_state, function_state);
let b = b.compile(options, global_state, function_state);
@ -76,9 +72,7 @@ impl Command {
} else if b.is_empty() {
a.clone()
} else {
b.clone()
.apply_prefix(a.as_str())
.or_forbid_prefix(a.forbids_prefix())
a.clone() + b
}
})
})
@ -175,12 +169,12 @@ impl From<&mut Function> for Command {
}
}
fn compile_debug(message: &MacroString, option: &CompileOptions) -> Vec<CompiledCommand> {
fn compile_debug(message: &MacroString, option: &CompileOptions) -> Vec<String> {
if option.debug {
vec![CompiledCommand::new(format!(
vec![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()
}
@ -192,52 +186,51 @@ fn compile_group(
options: &CompileOptions,
global_state: &MutCompilerState,
function_state: &FunctionCompilerState,
) -> Vec<CompiledCommand> {
) -> Vec<String> {
let command_count = commands
.iter()
.map(|cmd| cmd.get_count(options))
.sum::<usize>();
// only create a function if there are more than one command
match command_count {
0 => Vec::new(),
1 => commands[0].compile(options, global_state, function_state),
_ => {
let uid = function_state.request_uid();
let pass_macros = group_contains_macro(commands);
if command_count > 1 {
let uid = function_state.request_uid();
let pass_macros = group_contains_macro(commands);
// calculate a hashed path for the function in the `sb` subfolder
let function_path = {
let function_path = function_state.path();
let function_path = function_path.strip_prefix("sb/").unwrap_or(function_path);
// calculate a hashed path for the function in the `sb` subfolder
let function_path = {
let function_path = function_state.path();
let function_path = function_path.strip_prefix("sb/").unwrap_or(function_path);
let pre_hash_path = function_path.to_owned() + ":" + &uid.to_string();
let hash = md5::hash(pre_hash_path).to_hex_lowercase();
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]
};
"sb/".to_string() + function_path + "/" + &hash[..16]
};
let namespace = function_state.namespace();
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);
// 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 mut function_invocation = format!("function {namespace}:{function_path}");
if pass_macros {
// WARNING: this seems to be the only way to pass macros to the function called.
// Because everything is passed as a string, it looses one "level" of escaping per pass.
let macros_block = group_get_macros(commands)
.into_iter()
.map(|m| format!(r#"{m}:"$({m})""#))
.collect::<Vec<_>>()
.join(",");
function_invocation.push_str(&format!(" {{{macros_block}}}"));
}
vec![CompiledCommand::new(function_invocation).with_contains_macros(pass_macros)]
if pass_macros {
let macros_block = group_get_macros(commands)
.into_iter()
.map(|m| format!("{m}:$({m})"))
.collect::<Vec<_>>()
.join(",");
function_invocation.push_str(&format!(" {{{macros_block}}}"));
}
vec![function_invocation]
} else {
commands
.iter()
.flat_map(|cmd| cmd.compile(options, global_state, function_state))
.collect::<Vec<_>>()
}
}
@ -395,12 +388,12 @@ mod tests {
assert_eq!(
command_a.compile(options, global_state, function_state),
vec![CompiledCommand::new("say Hello, world!")]
vec!["say Hello, world!".to_string()]
);
assert_eq!(command_a.get_count(options), 1);
assert_eq!(
command_b.compile(options, global_state, function_state),
vec![CompiledCommand::new("say foo bar")]
vec!["say foo bar".to_string()]
);
assert_eq!(command_b.get_count(options), 1);
}
@ -415,7 +408,7 @@ mod tests {
assert_eq!(
comment.compile(options, global_state, function_state),
vec![CompiledCommand::new("#this is a comment").with_forbid_prefix(true)]
vec!["#this is a comment".to_string()]
);
assert_eq!(comment.get_count(options), 0);
}

View File

@ -68,20 +68,18 @@ impl Function {
if c.contains_macro() {
cmds.into_iter()
.map(|c| {
if c.contains_macros() {
let content = format!("${c}");
c.with_command(content)
} else {
if c.starts_with('#') {
c
} else {
format!("${c}")
}
.to_string()
})
.collect::<Vec<_>>()
.collect()
} else {
cmds.into_iter().map(|c| c.to_string()).collect::<Vec<_>>()
cmds
}
})
.collect::<Vec<_>>()
.collect::<Vec<String>>()
.join("\n");
VFile::Text(content)
}

View File

@ -1,6 +1,6 @@
//! Compile options for the compiler.
use std::{fmt::Display, ops::Deref, sync::Mutex};
use std::sync::Mutex;
use getset::Getters;
@ -86,127 +86,3 @@ impl FunctionCompilerState {
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())
}
}