diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 845e627..e81c3d4 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -3,7 +3,7 @@ on: push: branches: - main - - development + - develop - 'releases/**' pull_request: diff --git a/CHANGELOG.md b/CHANGELOG.md index 58c20c5..df47023 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,9 +9,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - support for commands using macros +- support for registering scoreboards (automatic creation and deletion) ### Changed - use "return" command for conditionals instead of data storage when using supported pack format +- update latest datapack format to 61 ### Removed diff --git a/Cargo.toml b/Cargo.toml index e2be020..ad98ab6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,12 +20,16 @@ serde = ["dep:serde"] zip = ["dep:zip"] [dependencies] -chksum-md5 = "0.0.0" -getset = "0.1.2" -serde = { version = "1.0.197", optional = true, features = ["derive"] } -serde_json = "1.0.114" -tracing = "0.1.40" -zip = { version = "2.1.3", default-features = false, features = ["deflate", "time"], optional = true } +chksum-md5 = "0.1.0" +getset = "0.1.5" +serde = { version = "1.0.219", optional = true, features = ["derive"] } +serde_json = "1.0.140" +tracing = "0.1.41" +zip = { version = "2.6.1", default-features = false, features = ["deflate", "time"], optional = true } [dev-dependencies] -tempfile = "3.13.0" +tempfile = "3.19.1" + +[package.metadata.docs.rs] +all-features = true +rustdoc-args = ["--cfg", "docsrs"] diff --git a/README.md b/README.md index c819828..8869d5a 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ use shulkerbox::{ util::compile::CompileOptions, }; -let mut dp = Datapack::new("shulkerpack", 20) // Create a new datapack with the name "shulkerpack" and the pack format 20 +let mut dp = Datapack::new("shulker", 20) // Create a new datapack with the name "shulkerpack" and the pack format 20 .with_description("I created this datapack with rust") // Add a description to the datapack .with_supported_formats(16..=20) // Add the supported formats of the datapack .with_template_folder(Path::new("./template")) // Add a template folder to the datapack. This will include all files in the template folder in the root of the datapack and can be used for including the "pack.png" file diff --git a/examples/basic.rs b/examples/basic.rs index 551c9d0..a54ae76 100644 --- a/examples/basic.rs +++ b/examples/basic.rs @@ -5,10 +5,12 @@ use shulkerbox::prelude::*; fn main() { // create a new datapack - let mut dp = Datapack::new(16).with_supported_formats(16..=20); + let mut dp = Datapack::new("example", 16).with_supported_formats(16..=20); - // get the namespace "test" - let namespace = dp.namespace_mut("test"); + dp.register_scoreboard("example_scoreboard", None::<&str>, None::<&str>); + + // get the namespace "example" + let namespace = dp.namespace_mut("example"); // get the function "foo" of the namespace "test" and add some commands let foo_function = namespace.function_mut("foo"); @@ -32,7 +34,7 @@ fn main() { )), ))); - dp.add_load("test:foo"); + dp.add_load("example:foo"); // compile the datapack let v_folder = dp.compile(&CompileOptions::default()); diff --git a/examples/improved_if_else.rs b/examples/improved_if_else.rs new file mode 100644 index 0000000..0e04aa9 --- /dev/null +++ b/examples/improved_if_else.rs @@ -0,0 +1,31 @@ +use std::path::Path; + +// import the prelude to get all the necessary structs +use shulkerbox::prelude::*; + +fn main() { + let mut dp = Datapack::new("main", 20).with_supported_formats(16..=20); + + // get the namespace "test" + let namespace = dp.namespace_mut("test"); + + // get the function "foo" of the namespace "test" and add some commands + let foo_function = namespace.function_mut("foo"); + + let ex = Execute::If( + Condition::from("entity A"), + Box::new(Execute::Run(Box::new("say A".into()))), + Some(Box::new(Execute::If( + Condition::from("entity B"), + Box::new(Execute::Run(Box::new("say B".into()))), + Some(Box::new(Execute::Run(Box::new("say C".into())))), + ))), + ); + foo_function.add_command(ex); + + // compile the datapack + let v_folder = dp.compile(&CompileOptions::default()); + + // place the compiled datapack in the "./dist" folder + v_folder.place(Path::new("./dist")).unwrap(); +} diff --git a/src/datapack/command/execute/conditional.rs b/src/datapack/command/execute/conditional.rs index 231bc3b..e6a793d 100644 --- a/src/datapack/command/execute/conditional.rs +++ b/src/datapack/command/execute/conditional.rs @@ -439,7 +439,7 @@ impl Condition { #[must_use] pub fn to_truth_table(&self) -> Vec { match self.normalize() { - Self::Atom(_) | Self::Not(_) => vec![self.clone()], + Self::Atom(_) | Self::Not(_) => vec![self.normalize()], Self::Or(a, b) => a .to_truth_table() .into_iter() @@ -461,7 +461,7 @@ impl Condition { /// 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` or double nested `Not` variant. Use `compile` instead. fn str_cond(&self) -> Option { match self { Self::Atom(s) => Some(MacroString::from("if ") + s.clone()), diff --git a/src/datapack/command/execute/mod.rs b/src/datapack/command/execute/mod.rs index 4ac218b..b56f4ca 100644 --- a/src/datapack/command/execute/mod.rs +++ b/src/datapack/command/execute/mod.rs @@ -234,7 +234,7 @@ impl Execute { Self::If(_, next, el) => { pack_formats.start() >= &4 && next.validate(pack_formats) - && el.as_deref().map_or(true, |el| el.validate(pack_formats)) + && el.as_deref().is_none_or(|el| el.validate(pack_formats)) } Self::Summon(_, next) | Self::On(_, next) => { pack_formats.start() >= &12 && next.validate(pack_formats) @@ -261,7 +261,7 @@ impl Execute { Self::If(cond, then, el) => { cond.contains_macro() || then.contains_macro() - || el.as_deref().map_or(false, Self::contains_macro) + || el.as_deref().is_some_and(Self::contains_macro) } Self::Run(cmd) => cmd.contains_macro(), Self::Runs(cmds) => cmds.iter().any(super::Command::contains_macro), diff --git a/src/datapack/command/mod.rs b/src/datapack/command/mod.rs index eb8f291..30387ee 100644 --- a/src/datapack/command/mod.rs +++ b/src/datapack/command/mod.rs @@ -363,8 +363,8 @@ fn validate_raw_cmd(cmd: &str, pack_formats: &RangeInclusive) -> bool { map }); - cmd.split_ascii_whitespace().next().map_or(true, |cmd| { - cmd_formats.get(cmd).map_or(true, |range| { + cmd.split_ascii_whitespace().next().is_none_or(|cmd| { + cmd_formats.get(cmd).is_none_or(|range| { let start_cmd = range.start(); let end_cmd = range.end(); diff --git a/src/datapack/function.rs b/src/datapack/function.rs index e5d627c..637f0e0 100644 --- a/src/datapack/function.rs +++ b/src/datapack/function.rs @@ -25,11 +25,11 @@ pub struct Function { } impl Function { - pub(in crate::datapack) fn new(namespace: &str, name: &str) -> Self { + pub(in crate::datapack) fn new(namespace: impl Into, name: impl Into) -> Self { Self { commands: Vec::new(), - name: name.to_string(), - namespace: namespace.to_string(), + name: name.into(), + namespace: namespace.into(), } } /// Add a command to the function. diff --git a/src/datapack/mod.rs b/src/datapack/mod.rs index eb20440..d1fe153 100644 --- a/src/datapack/mod.rs +++ b/src/datapack/mod.rs @@ -8,7 +8,7 @@ pub use command::{Command, Condition, Execute}; pub use function::Function; pub use namespace::Namespace; -use std::{collections::HashMap, ops::RangeInclusive, sync::Mutex}; +use std::{borrow::Cow, collections::BTreeMap, ops::RangeInclusive, sync::Mutex}; use crate::{ util::compile::{CompileOptions, CompilerState, MutCompilerState}, @@ -23,21 +23,28 @@ pub struct Datapack { description: String, pack_format: u8, supported_formats: Option>, - namespaces: HashMap, + main_namespace_name: String, + namespaces: BTreeMap, + /// Scoreboard name -> (criteria, display name) + scoreboards: BTreeMap, Option)>, + uninstall_commands: Vec, custom_files: VFolder, } impl Datapack { - pub const LATEST_FORMAT: u8 = 48; + pub const LATEST_FORMAT: u8 = 61; /// Create a new Minecraft datapack. #[must_use] - pub fn new(pack_format: u8) -> Self { + pub fn new(main_namespace_name: impl Into, pack_format: u8) -> Self { Self { description: String::from("A Minecraft datapack created with shulkerbox"), pack_format, supported_formats: None, - namespaces: HashMap::new(), + main_namespace_name: main_namespace_name.into(), + namespaces: BTreeMap::new(), + scoreboards: BTreeMap::new(), + uninstall_commands: Vec::new(), custom_files: VFolder::new(), } } @@ -65,6 +72,7 @@ impl Datapack { /// # Errors /// - If loading the directory fails #[cfg(feature = "fs_access")] + #[cfg_attr(docsrs, doc(cfg(feature = "fs_access")))] pub fn with_template_folder

(self, path: P) -> std::io::Result where P: AsRef, @@ -90,24 +98,49 @@ impl Datapack { } /// Mutably get a namespace by name or create a new one if it doesn't exist. - pub fn namespace_mut(&mut self, name: &str) -> &mut Namespace { + pub fn namespace_mut(&mut self, name: impl Into) -> &mut Namespace { + let name = name.into(); self.namespaces - .entry(name.to_string()) + .entry(name.clone()) .or_insert_with(|| Namespace::new(name)) } /// Add a function to the tick function list. - pub fn add_tick(&mut self, function: &str) { + pub fn add_tick(&mut self, function: impl Into) { self.namespace_mut("minecraft") .tag_mut("tick", tag::TagType::Function) - .add_value(tag::TagValue::Simple(function.to_string())); + .add_value(tag::TagValue::Simple(function.into())); } /// Add a function to the load function list. - pub fn add_load(&mut self, function: &str) { + pub fn add_load(&mut self, function: impl Into) { self.namespace_mut("minecraft") .tag_mut("load", tag::TagType::Function) - .add_value(tag::TagValue::Simple(function.to_string())); + .add_value(tag::TagValue::Simple(function.into())); + } + + /// Register a scoreboard. + pub fn register_scoreboard( + &mut self, + name: impl Into, + criteria: Option>, + display_name: Option>, + ) { + self.scoreboards.insert( + name.into(), + (criteria.map(Into::into), display_name.map(Into::into)), + ); + } + + /// Scoreboards registered in the datapack. + #[must_use] + pub fn scoreboards(&self) -> &BTreeMap, Option)> { + &self.scoreboards + } + + /// Add commands to the uninstall function. + pub fn add_uninstall_commands(&mut self, commands: Vec) { + self.uninstall_commands.extend(commands); } /// Add a custom file to the datapack. @@ -132,8 +165,71 @@ impl Datapack { root_folder.add_file("pack.mcmeta", mcmeta); let mut data_folder = VFolder::new(); + let mut modified_namespaces = self + .namespaces + .iter() + .map(|(name, namespace)| (name.as_str(), Cow::Borrowed(namespace))) + .collect::>(); + + let mut uninstall_commands = options + .uninstall_function + .then_some(Cow::Borrowed(&self.uninstall_commands)); + + if !self.scoreboards.is_empty() { + let main_namespace = modified_namespaces + .entry(&self.main_namespace_name) + .or_insert_with(|| Cow::Owned(Namespace::new(&self.main_namespace_name))); + let register_scoreboard_function = main_namespace + .to_mut() + .function_mut("sb/register_scoreboards"); + for (name, (criteria, display_name)) in &self.scoreboards { + let mut creation_command = format!( + "scoreboard objectives add {name} {criteria}", + criteria = criteria.as_deref().unwrap_or("dummy") + ); + if let Some(display_name) = display_name { + creation_command.push(' '); + creation_command.push_str(display_name); + } + register_scoreboard_function.add_command(Command::Raw(creation_command)); + + if let Some(uninstall_commands) = uninstall_commands.as_mut() { + uninstall_commands + .to_mut() + .push(Command::Raw(format!("scoreboard objectives remove {name}"))); + } + } + + let minecraft_namespace = modified_namespaces + .entry("minecraft") + .or_insert_with(|| Cow::Owned(Namespace::new("minecraft"))); + minecraft_namespace + .to_mut() + .tag_mut("load", tag::TagType::Function) + .values_mut() + .insert( + 0, + tag::TagValue::Simple(format!( + "{}:sb/register_scoreboards", + self.main_namespace_name + )), + ); + } + + if let Some(uninstall_commands) = uninstall_commands { + if !uninstall_commands.is_empty() { + let main_namespace = modified_namespaces + .entry(&self.main_namespace_name) + .or_insert_with(|| Cow::Owned(Namespace::new(&self.main_namespace_name))); + let uninstall_function = main_namespace.to_mut().function_mut("uninstall"); + uninstall_function + .get_commands_mut() + .extend(uninstall_commands.into_owned()); + } + } + // Compile namespaces - for (name, namespace) in &self.namespaces { + for (name, namespace) in modified_namespaces { let namespace_folder = namespace.compile(&options, &compiler_state); data_folder.add_existing_folder(name, namespace_folder); } @@ -180,7 +276,7 @@ mod tests { fn test_datapack() { let template_dir = tempfile::tempdir().expect("error creating tempdir"); - let mut dp = Datapack::new(Datapack::LATEST_FORMAT) + let mut dp = Datapack::new("main", Datapack::LATEST_FORMAT) .with_description("My datapack") .with_template_folder(template_dir.path()) .expect("error reading template folder"); @@ -193,7 +289,7 @@ mod tests { #[test] fn test_generate_mcmeta() { - let dp = &Datapack::new(Datapack::LATEST_FORMAT).with_description("foo"); + let dp = &Datapack::new("main", Datapack::LATEST_FORMAT).with_description("foo"); let state = Mutex::new(CompilerState::default()); let mcmeta = generate_mcmeta(dp, &CompileOptions::default(), &state); diff --git a/src/datapack/namespace.rs b/src/datapack/namespace.rs index 99f53c3..aa2e295 100644 --- a/src/datapack/namespace.rs +++ b/src/datapack/namespace.rs @@ -28,9 +28,9 @@ pub struct Namespace { impl Namespace { /// Create a new namespace. - pub(in crate::datapack) fn new(name: &str) -> Self { + pub(in crate::datapack) fn new(name: impl Into) -> Self { Self { - name: name.to_string(), + name: name.into(), functions: HashMap::new(), tags: HashMap::new(), } @@ -62,23 +62,24 @@ impl Namespace { /// Mutably get a function by name or create a new one if it doesn't exist. #[must_use] - pub fn function_mut(&mut self, name: &str) -> &mut Function { + pub fn function_mut(&mut self, name: impl Into) -> &mut Function { + let name = name.into(); self.functions - .entry(name.to_string()) + .entry(name.clone()) .or_insert_with(|| Function::new(&self.name, name)) } /// Get a tag by name and type. #[must_use] - pub fn tag(&self, name: &str, tag_type: TagType) -> Option<&Tag> { - self.tags.get(&(name.to_string(), tag_type)) + pub fn tag(&self, name: impl Into, tag_type: TagType) -> Option<&Tag> { + self.tags.get(&(name.into(), tag_type)) } /// Mutably get a tag by name and type or create a new one if it doesn't exist. #[must_use] - pub fn tag_mut(&mut self, name: &str, tag_type: TagType) -> &mut Tag { + pub fn tag_mut(&mut self, name: impl Into, tag_type: TagType) -> &mut Tag { self.tags - .entry((name.to_string(), tag_type)) + .entry((name.into(), tag_type)) .or_insert_with(|| Tag::new(false)) } diff --git a/src/datapack/tag.rs b/src/datapack/tag.rs index ee95cdd..5da9902 100644 --- a/src/datapack/tag.rs +++ b/src/datapack/tag.rs @@ -37,10 +37,16 @@ impl Tag { /// Get the values of the tag. #[must_use] - pub fn get_values(&self) -> &Vec { + pub fn values(&self) -> &[TagValue] { &self.values } + /// Get a mutable reference to the values of the tag. + #[must_use] + pub fn values_mut(&mut self) -> &mut Vec { + &mut self.values + } + /// Add a value to the tag. pub fn add_value(&mut self, value: TagValue) { self.values.push(value); @@ -186,7 +192,7 @@ mod tests { required: true, }); - assert_eq!(tag.get_values().len(), 2); + assert_eq!(tag.values().len(), 2); let compiled = tag.compile(&CompileOptions::default(), &MutCompilerState::default()); diff --git a/src/lib.rs b/src/lib.rs index 0330fec..c54ee34 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,6 +10,7 @@ )] #![warn(clippy::all, clippy::pedantic, clippy::perf)] #![allow(clippy::missing_panics_doc, clippy::missing_const_for_fn)] +#![cfg_attr(docsrs, feature(doc_cfg))] pub mod datapack; pub mod util; diff --git a/src/util/compile.rs b/src/util/compile.rs index 374aa65..78ee3ce 100644 --- a/src/util/compile.rs +++ b/src/util/compile.rs @@ -17,6 +17,8 @@ pub struct CompileOptions { pub(crate) pack_format: u8, /// Whether to compile in debug mode. pub(crate) debug: bool, + /// Whether to generate an uninstall function. + pub(crate) uninstall_function: bool, } impl CompileOptions { @@ -25,6 +27,15 @@ impl CompileOptions { pub fn with_debug(self, debug: bool) -> Self { Self { debug, ..self } } + + /// Set whether to generate an uninstall function. + #[must_use] + pub fn with_uninstall_function(self, uninstall_function: bool) -> Self { + Self { + uninstall_function, + ..self + } + } } impl Default for CompileOptions { @@ -32,6 +43,7 @@ impl Default for CompileOptions { Self { pack_format: Datapack::LATEST_FORMAT, debug: true, + uninstall_function: true, } } } diff --git a/src/virtual_fs.rs b/src/virtual_fs.rs index faf6c47..ec29335 100644 --- a/src/virtual_fs.rs +++ b/src/virtual_fs.rs @@ -131,6 +131,7 @@ impl VFolder { /// # Errors /// - If the folder cannot be written #[cfg(feature = "fs_access")] + #[cfg_attr(docsrs, doc(cfg(feature = "fs_access")))] pub fn place

(&self, path: P) -> std::io::Result<()> where P: AsRef, @@ -162,6 +163,7 @@ impl VFolder { /// # Errors /// - If the zip archive cannot be written #[cfg(all(feature = "fs_access", feature = "zip"))] + #[cfg_attr(docsrs, doc(cfg(all(feature = "fs_access", feature = "zip"))))] pub fn zip

(&self, path: P) -> std::io::Result<()> where P: AsRef, @@ -174,6 +176,7 @@ impl VFolder { /// # Errors /// - If the zip archive cannot be written #[cfg(all(feature = "fs_access", feature = "zip"))] + #[cfg_attr(docsrs, doc(cfg(all(feature = "fs_access", feature = "zip"))))] pub fn zip_with_comment(&self, path: P, comment: S) -> std::io::Result<()> where P: AsRef, @@ -246,14 +249,14 @@ impl VFolder { /// Recursively merge another folder into this folder. /// Returns a list of paths that were replaced by other. pub fn merge(&mut self, other: Self) -> Vec { - self._merge(other, "") + self.merge_(other, "") } - fn _merge(&mut self, other: Self, prefix: &str) -> Vec { + fn merge_(&mut self, other: Self, prefix: &str) -> Vec { let mut replaced = Vec::new(); for (name, folder) in other.folders { if let Some(existing_folder) = self.folders.get_mut(&name) { - let replaced_folder = existing_folder._merge(folder, &format!("{prefix}{name}/")); + let replaced_folder = existing_folder.merge_(folder, &format!("{prefix}{name}/")); replaced.extend(replaced_folder); } else { self.folders.insert(name, folder); @@ -271,6 +274,7 @@ impl VFolder { } #[cfg(feature = "fs_access")] +#[cfg_attr(docsrs, doc(cfg(feature = "fs_access")))] impl TryFrom<&std::path::Path> for VFolder { type Error = std::io::Error; @@ -374,6 +378,7 @@ impl From<&[u8]> for VFile { } #[cfg(feature = "fs_access")] +#[cfg_attr(docsrs, doc(cfg(feature = "fs_access")))] impl TryFrom<&std::path::Path> for VFile { type Error = std::io::Error;