551 lines
18 KiB
Rust
551 lines
18 KiB
Rust
use std::ops::{BitAnd, BitOr, Not};
|
|
|
|
use chksum_md5 as md5;
|
|
|
|
use super::Command;
|
|
use crate::util::{
|
|
compile::{CompileOptions, FunctionCompilerState, MutCompilerState},
|
|
ExtendableQueue,
|
|
};
|
|
|
|
#[allow(missing_docs)]
|
|
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
|
#[derive(Debug, Clone)]
|
|
pub enum Execute {
|
|
Align(String, Box<Execute>),
|
|
Anchored(String, Box<Execute>),
|
|
As(String, Box<Execute>),
|
|
At(String, Box<Execute>),
|
|
AsAt(String, Box<Execute>),
|
|
Facing(String, Box<Execute>),
|
|
In(String, Box<Execute>),
|
|
On(String, Box<Execute>),
|
|
Positioned(String, Box<Execute>),
|
|
Rotated(String, Box<Execute>),
|
|
Store(String, Box<Execute>),
|
|
Summon(String, Box<Execute>),
|
|
If(Condition, Box<Execute>, Option<Box<Execute>>),
|
|
Run(Box<Command>),
|
|
Runs(Vec<Command>),
|
|
}
|
|
|
|
impl Execute {
|
|
/// Compile the execute command into a list of strings.
|
|
pub fn compile(
|
|
&self,
|
|
options: &CompileOptions,
|
|
global_state: &MutCompilerState,
|
|
function_state: &FunctionCompilerState,
|
|
) -> Vec<String> {
|
|
// Directly compile the command if it is a run command, skipping the execute part
|
|
// Otherwise, compile the execute command using internal function
|
|
if let Self::Run(cmd) = self {
|
|
cmd.compile(options, global_state, function_state)
|
|
} else {
|
|
self.compile_internal(
|
|
String::from("execute "),
|
|
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.
|
|
fn compile_internal(
|
|
&self,
|
|
prefix: String,
|
|
require_grouping: bool,
|
|
options: &CompileOptions,
|
|
global_state: &MutCompilerState,
|
|
function_state: &FunctionCompilerState,
|
|
) -> Vec<(bool, String)> {
|
|
match self {
|
|
Self::Align(arg, next)
|
|
| Self::Anchored(arg, next)
|
|
| Self::As(arg, next)
|
|
| Self::At(arg, next)
|
|
| Self::Facing(arg, next)
|
|
| Self::In(arg, next)
|
|
| Self::On(arg, next)
|
|
| Self::Positioned(arg, next)
|
|
| Self::Rotated(arg, next)
|
|
| Self::Store(arg, next)
|
|
| Self::Summon(arg, next) => next.compile_internal(
|
|
format!("{prefix}{op} {arg} ", op = self.variant_name()),
|
|
require_grouping,
|
|
options,
|
|
global_state,
|
|
function_state,
|
|
),
|
|
Self::AsAt(selector, next) => next.compile_internal(
|
|
format!("{prefix}as {selector} at @s "),
|
|
require_grouping,
|
|
options,
|
|
global_state,
|
|
function_state,
|
|
),
|
|
Self::If(cond, then, el) => compile_if_cond(
|
|
cond,
|
|
then.as_ref(),
|
|
el.as_deref(),
|
|
&prefix,
|
|
options,
|
|
global_state,
|
|
function_state,
|
|
),
|
|
Self::Run(command) => match &**command {
|
|
Command::Execute(ex) => ex.compile_internal(
|
|
prefix,
|
|
require_grouping,
|
|
options,
|
|
global_state,
|
|
function_state,
|
|
),
|
|
command => command
|
|
.compile(options, global_state, function_state)
|
|
.into_iter()
|
|
.map(|c| map_run_cmd(c, &prefix))
|
|
.collect(),
|
|
},
|
|
Self::Runs(commands) if !require_grouping => commands
|
|
.iter()
|
|
.flat_map(|c| c.compile(options, global_state, function_state))
|
|
.map(|c| map_run_cmd(c, &prefix))
|
|
.collect(),
|
|
Self::Runs(commands) => Command::Group(commands.clone())
|
|
.compile(options, global_state, function_state)
|
|
.into_iter()
|
|
.map(|c| map_run_cmd(c, &prefix))
|
|
.collect(),
|
|
}
|
|
}
|
|
|
|
/// Get the count of the commands the execute command will compile into.
|
|
#[tracing::instrument(skip(options))]
|
|
pub(super) fn get_count(&self, options: &CompileOptions) -> usize {
|
|
let global_state = MutCompilerState::default();
|
|
let function_state =
|
|
FunctionCompilerState::new("[INTERNAL]", "[INTERNAL]", ExtendableQueue::default());
|
|
|
|
self.compile_internal(
|
|
String::new(),
|
|
false,
|
|
options,
|
|
&global_state,
|
|
&function_state,
|
|
)
|
|
.len()
|
|
}
|
|
|
|
/// Get the variant name of the execute command.
|
|
#[must_use]
|
|
pub fn variant_name(&self) -> &str {
|
|
match self {
|
|
Self::Align(..) => "align",
|
|
Self::Anchored(..) => "anchored",
|
|
Self::As(..) => "as",
|
|
Self::At(..) => "at",
|
|
Self::AsAt(..) => "as_at",
|
|
Self::Facing(..) => "facing",
|
|
Self::In(..) => "in",
|
|
Self::On(..) => "on",
|
|
Self::Positioned(..) => "positioned",
|
|
Self::Rotated(..) => "rotated",
|
|
Self::Store(..) => "store",
|
|
Self::Summon(..) => "summon",
|
|
Self::If(..) => "if",
|
|
Self::Run(..) => "run",
|
|
Self::Runs(..) => "runs",
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 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(cmd: String, prefix: &str) -> (bool, String) {
|
|
if cmd.starts_with('#') {
|
|
(false, cmd)
|
|
} else {
|
|
(true, prefix.to_string() + "run " + &cmd)
|
|
}
|
|
}
|
|
|
|
/// Compile an if condition command.
|
|
/// The first tuple element is a boolean indicating if the prefix should be used for that command.
|
|
#[tracing::instrument(skip_all)]
|
|
fn compile_if_cond(
|
|
cond: &Condition,
|
|
then: &Execute,
|
|
el: Option<&Execute>,
|
|
prefix: &str,
|
|
options: &CompileOptions,
|
|
global_state: &MutCompilerState,
|
|
function_state: &FunctionCompilerState,
|
|
) -> Vec<(bool, String)> {
|
|
let then_count = then.get_count(options);
|
|
|
|
let str_cond = cond.clone().compile(options, global_state, function_state);
|
|
let require_grouping_uid = (el.is_some() || then_count > 1).then(|| {
|
|
// calculate a unique condition id for the else check
|
|
let uid = function_state.request_uid();
|
|
let pre_hash = function_state.path().to_owned() + ":" + &uid.to_string();
|
|
|
|
md5::hash(pre_hash).to_hex_lowercase()
|
|
});
|
|
#[allow(clippy::option_if_let_else)]
|
|
let then = if let Some(success_uid) = require_grouping_uid.as_deref() {
|
|
// prepare commands for grouping
|
|
let mut group_cmd = match then.clone() {
|
|
Execute::Run(cmd) => vec![*cmd],
|
|
Execute::Runs(cmds) => cmds,
|
|
ex => vec![Command::Execute(ex)],
|
|
};
|
|
// add success condition to the group
|
|
// this condition will be checked after the group ran to determine if the else part should be executed
|
|
if el.is_some() && str_cond.len() <= 1 {
|
|
group_cmd.push(
|
|
format!("data modify storage shulkerbox:cond {success_uid} set value true")
|
|
.as_str()
|
|
.into(),
|
|
);
|
|
}
|
|
Command::Group(group_cmd)
|
|
.compile(options, global_state, function_state)
|
|
.iter()
|
|
.map(|s| (true, "run ".to_string() + s))
|
|
.collect()
|
|
} else {
|
|
then.compile_internal(
|
|
String::new(),
|
|
require_grouping_uid.is_some(),
|
|
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(|| {
|
|
let success_uid = require_grouping_uid.as_deref().unwrap_or_else(|| {
|
|
tracing::error!("No success_uid found for each_or_cmd, using default");
|
|
"if_success"
|
|
});
|
|
(
|
|
format!("data modify storage shulkerbox:cond {success_uid} set value true"),
|
|
combine_conditions_commands(
|
|
str_cond.clone(),
|
|
&[(
|
|
true,
|
|
format!("run data modify storage shulkerbox:cond {success_uid} set value true"),
|
|
)],
|
|
),
|
|
)
|
|
});
|
|
// build the condition for each then command
|
|
let successful_cond = if each_or_cmd.is_some() {
|
|
let success_uid = require_grouping_uid.as_deref().unwrap_or_else(|| {
|
|
tracing::error!("No success_uid found for each_or_cmd, using default");
|
|
"if_success"
|
|
});
|
|
Condition::Atom(format!("data storage shulkerbox:cond {{{success_uid}:1b}}")).compile(
|
|
options,
|
|
global_state,
|
|
function_state,
|
|
)
|
|
} else {
|
|
str_cond
|
|
};
|
|
// combine the conditions with the then commands
|
|
let then_commands = combine_conditions_commands(successful_cond, &then);
|
|
// build the else part
|
|
let el_commands = el
|
|
.map(|el| {
|
|
let success_uid = require_grouping_uid.as_deref().unwrap_or_else(|| {
|
|
tracing::error!("No success_uid found for each_or_cmd, using default");
|
|
"if_success"
|
|
});
|
|
let else_cond =
|
|
(!Condition::Atom(format!("data storage shulkerbox:cond {{{success_uid}:1b}}")))
|
|
.compile(options, global_state, function_state);
|
|
let el = el.compile_internal(
|
|
String::new(),
|
|
else_cond.len() > 1,
|
|
options,
|
|
global_state,
|
|
function_state,
|
|
);
|
|
combine_conditions_commands(else_cond, &el)
|
|
})
|
|
.unwrap_or_default();
|
|
|
|
// reset the success storage if needed
|
|
let reset_success_storage = if each_or_cmd.is_some() || el.is_some() {
|
|
let success_uid = require_grouping_uid.as_deref().unwrap_or_else(|| {
|
|
tracing::error!("No success_uid found for each_or_cmd, using default");
|
|
"if_success"
|
|
});
|
|
Some((
|
|
false,
|
|
format!("data remove storage shulkerbox:cond {success_uid}"),
|
|
))
|
|
} else {
|
|
None
|
|
};
|
|
|
|
// combine all parts
|
|
reset_success_storage
|
|
.clone()
|
|
.into_iter()
|
|
.chain(each_or_cmd.map(|(_, cmds)| cmds).unwrap_or_default())
|
|
.chain(then_commands)
|
|
.chain(el_commands)
|
|
.chain(reset_success_storage)
|
|
.map(|(use_prefix, cmd)| {
|
|
let cmd = if use_prefix {
|
|
prefix.to_string() + &cmd
|
|
} else {
|
|
cmd
|
|
};
|
|
(use_prefix, cmd)
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
fn combine_conditions_commands(
|
|
conditions: Vec<String>,
|
|
commands: &[(bool, String)],
|
|
) -> Vec<(bool, String)> {
|
|
conditions
|
|
.into_iter()
|
|
.flat_map(|cond| {
|
|
commands.iter().map(move |(use_prefix, cmd)| {
|
|
// combine the condition with the command if it uses a prefix
|
|
let cmd = if *use_prefix {
|
|
cond.clone() + " " + cmd
|
|
} else {
|
|
cmd.clone()
|
|
};
|
|
(*use_prefix, cmd)
|
|
})
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
#[allow(missing_docs)]
|
|
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
|
#[derive(Debug, Clone, PartialEq)]
|
|
pub enum Condition {
|
|
Atom(String),
|
|
Not(Box<Condition>),
|
|
And(Box<Condition>, Box<Condition>),
|
|
Or(Box<Condition>, Box<Condition>),
|
|
}
|
|
impl Condition {
|
|
/// Normalize the condition to eliminate complex negations.
|
|
/// Uses De Morgan's laws to simplify the condition.
|
|
#[must_use]
|
|
pub fn normalize(&self) -> Self {
|
|
match self {
|
|
Self::Atom(_) => self.clone(),
|
|
Self::Not(c) => match *c.clone() {
|
|
Self::Atom(c) => Self::Not(Box::new(Self::Atom(c))),
|
|
Self::Not(c) => c.normalize(),
|
|
Self::And(a, b) => ((!*a).normalize()) | ((!*b).normalize()),
|
|
Self::Or(a, b) => ((!*a).normalize()) & ((!*b).normalize()),
|
|
},
|
|
Self::And(a, b) => a.normalize() & b.normalize(),
|
|
Self::Or(a, b) => a.normalize() | b.normalize(),
|
|
}
|
|
}
|
|
|
|
/// Convert the condition into a truth table.
|
|
/// This will expand the condition into all possible combinations of its atoms.
|
|
/// All vector elements are in disjunction with each other and do not contain disjunctions and complex negations in them.
|
|
#[must_use]
|
|
pub fn to_truth_table(&self) -> Vec<Self> {
|
|
match self.normalize() {
|
|
Self::Atom(_) | Self::Not(_) => vec![self.clone()],
|
|
Self::Or(a, b) => a
|
|
.to_truth_table()
|
|
.into_iter()
|
|
.chain(b.to_truth_table())
|
|
.collect(),
|
|
Self::And(a, b) => {
|
|
let a = a.to_truth_table();
|
|
let b = b.to_truth_table();
|
|
|
|
a.into_iter()
|
|
.flat_map(|el1| {
|
|
b.iter()
|
|
.map(move |el2| Self::And(Box::new(el1.clone()), Box::new(el2.clone())))
|
|
})
|
|
.collect()
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Convert the condition into a string.
|
|
///
|
|
/// Will fail if the condition contains an `Or` variant. Use `compile` instead.
|
|
fn str_cond(&self) -> Option<String> {
|
|
match self {
|
|
Self::Atom(s) => Some("if ".to_string() + &s),
|
|
Self::Not(n) => match *(*n).clone() {
|
|
Self::Atom(s) => Some("unless ".to_string() + &s),
|
|
_ => None,
|
|
},
|
|
Self::And(a, b) => {
|
|
let a = a.str_cond()?;
|
|
let b = b.str_cond()?;
|
|
|
|
Some(a + " " + &b)
|
|
}
|
|
Self::Or(..) => None,
|
|
}
|
|
}
|
|
|
|
/// Compile the condition into a list of strings that can be used in Minecraft.
|
|
#[allow(clippy::only_used_in_recursion)]
|
|
pub fn compile(
|
|
&self,
|
|
_options: &CompileOptions,
|
|
_global_state: &MutCompilerState,
|
|
_function_state: &FunctionCompilerState,
|
|
) -> Vec<String> {
|
|
let truth_table = self.to_truth_table();
|
|
|
|
truth_table
|
|
.into_iter()
|
|
.map(|c| {
|
|
c.str_cond()
|
|
.expect("Truth table should not contain Or variants")
|
|
})
|
|
.collect()
|
|
}
|
|
}
|
|
|
|
impl From<&str> for Condition {
|
|
fn from(s: &str) -> Self {
|
|
Self::Atom(s.to_string())
|
|
}
|
|
}
|
|
|
|
impl Not for Condition {
|
|
type Output = Self;
|
|
|
|
fn not(self) -> Self {
|
|
Self::Not(Box::new(self))
|
|
}
|
|
}
|
|
impl BitAnd for Condition {
|
|
type Output = Self;
|
|
|
|
fn bitand(self, rhs: Self) -> Self {
|
|
Self::And(Box::new(self), Box::new(rhs))
|
|
}
|
|
}
|
|
impl BitOr for Condition {
|
|
type Output = Self;
|
|
|
|
fn bitor(self, rhs: Self) -> Self {
|
|
Self::Or(Box::new(self), Box::new(rhs))
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[allow(clippy::redundant_clone)]
|
|
#[test]
|
|
fn test_condition() {
|
|
let c1 = Condition::Atom("foo".to_string());
|
|
let c2 = Condition::Atom("bar".to_string());
|
|
let c3 = Condition::Atom("baz".to_string());
|
|
|
|
assert_eq!(
|
|
(c1.clone() & c2.clone()).normalize(),
|
|
c1.clone() & c2.clone()
|
|
);
|
|
assert_eq!(
|
|
(c1.clone() & c2.clone() & c3.clone()).normalize(),
|
|
c1.clone() & c2.clone() & c3.clone()
|
|
);
|
|
assert_eq!(
|
|
(c1.clone() | c2.clone()).normalize(),
|
|
c1.clone() | c2.clone()
|
|
);
|
|
assert_eq!(
|
|
(c1.clone() | c2.clone() | c3.clone()).normalize(),
|
|
c1.clone() | c2.clone() | c3.clone()
|
|
);
|
|
assert_eq!(
|
|
(c1.clone() & c2.clone() | c3.clone()).normalize(),
|
|
c1.clone() & c2.clone() | c3.clone()
|
|
);
|
|
assert_eq!(
|
|
(c1.clone() | c2.clone() & c3.clone()).normalize(),
|
|
c1.clone() | c2.clone() & c3.clone()
|
|
);
|
|
assert_eq!(
|
|
(c1.clone() & c2.clone() | c3.clone() & c1.clone()).normalize(),
|
|
c1.clone() & c2.clone() | c3.clone() & c1.clone()
|
|
);
|
|
assert_eq!(
|
|
(!(c1.clone() | c2.clone())).normalize(),
|
|
!c1.clone() & !c2.clone()
|
|
);
|
|
assert_eq!(
|
|
(!(c1.clone() & c2.clone())).normalize(),
|
|
!c1.clone() | !c2.clone()
|
|
);
|
|
}
|
|
|
|
#[allow(clippy::redundant_clone)]
|
|
#[test]
|
|
fn test_truth_table() {
|
|
let c1 = Condition::Atom("foo".to_string());
|
|
let c2 = Condition::Atom("bar".to_string());
|
|
let c3 = Condition::Atom("baz".to_string());
|
|
let c4 = Condition::Atom("foobar".to_string());
|
|
|
|
assert_eq!(
|
|
(c1.clone() & c2.clone()).to_truth_table(),
|
|
vec![c1.clone() & c2.clone()]
|
|
);
|
|
|
|
assert_eq!(
|
|
(c1.clone() & c2.clone() & c3.clone()).to_truth_table(),
|
|
vec![c1.clone() & c2.clone() & c3.clone()]
|
|
);
|
|
|
|
assert_eq!(
|
|
(c1.clone() | c2.clone()).to_truth_table(),
|
|
vec![c1.clone(), c2.clone()]
|
|
);
|
|
|
|
assert_eq!(
|
|
((c1.clone() | c2.clone()) & c3.clone()).to_truth_table(),
|
|
vec![c1.clone() & c3.clone(), c2.clone() & c3.clone()]
|
|
);
|
|
|
|
assert_eq!(
|
|
((c1.clone() & c2.clone()) | c3.clone()).to_truth_table(),
|
|
vec![c1.clone() & c2.clone(), c3.clone()]
|
|
);
|
|
|
|
assert_eq!(
|
|
(c1.clone() & !(c2.clone() | (c3.clone() & c4.clone()))).to_truth_table(),
|
|
vec![
|
|
c1.clone() & (!c2.clone() & !c3.clone()),
|
|
c1.clone() & (!c2.clone() & !c4.clone())
|
|
]
|
|
);
|
|
}
|
|
}
|