Merge branch 'feature/scoreboard_management' into develop

This commit is contained in:
Moritz Hölting 2025-04-07 14:07:32 +02:00
commit ed62e75e89
16 changed files with 209 additions and 49 deletions

View File

@ -3,7 +3,7 @@ on:
push:
branches:
- main
- development
- develop
- 'releases/**'
pull_request:

View File

@ -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

View File

@ -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"]

View File

@ -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

View File

@ -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());

View File

@ -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();
}

View File

@ -439,7 +439,7 @@ impl Condition {
#[must_use]
pub fn to_truth_table(&self) -> Vec<Self> {
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<MacroString> {
match self {
Self::Atom(s) => Some(MacroString::from("if ") + s.clone()),

View File

@ -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),

View File

@ -363,8 +363,8 @@ fn validate_raw_cmd(cmd: &str, pack_formats: &RangeInclusive<u8>) -> 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();

View File

@ -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<String>, name: impl Into<String>) -> 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.

View File

@ -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<RangeInclusive<u8>>,
namespaces: HashMap<String, Namespace>,
main_namespace_name: String,
namespaces: BTreeMap<String, Namespace>,
/// Scoreboard name -> (criteria, display name)
scoreboards: BTreeMap<String, (Option<String>, Option<String>)>,
uninstall_commands: Vec<Command>,
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<String>, 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<P>(self, path: P) -> std::io::Result<Self>
where
P: AsRef<std::path::Path>,
@ -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<String>) -> &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<String>) {
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<String>) {
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<String>,
criteria: Option<impl Into<String>>,
display_name: Option<impl Into<String>>,
) {
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<String, (Option<String>, Option<String>)> {
&self.scoreboards
}
/// Add commands to the uninstall function.
pub fn add_uninstall_commands(&mut self, commands: Vec<Command>) {
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::<BTreeMap<_, _>>();
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);

View File

@ -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<String>) -> 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<String>) -> &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<String>, 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<String>, tag_type: TagType) -> &mut Tag {
self.tags
.entry((name.to_string(), tag_type))
.entry((name.into(), tag_type))
.or_insert_with(|| Tag::new(false))
}

View File

@ -37,10 +37,16 @@ impl Tag {
/// Get the values of the tag.
#[must_use]
pub fn get_values(&self) -> &Vec<TagValue> {
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<TagValue> {
&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());

View File

@ -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;

View File

@ -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,
}
}
}

View File

@ -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<P>(&self, path: P) -> std::io::Result<()>
where
P: AsRef<std::path::Path>,
@ -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<P>(&self, path: P) -> std::io::Result<()>
where
P: AsRef<std::path::Path>,
@ -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<P, S>(&self, path: P, comment: S) -> std::io::Result<()>
where
P: AsRef<std::path::Path>,
@ -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<String> {
self._merge(other, "")
self.merge_(other, "")
}
fn _merge(&mut self, other: Self, prefix: &str) -> Vec<String> {
fn merge_(&mut self, other: Self, prefix: &str) -> Vec<String> {
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;