diff --git a/Cargo.toml b/Cargo.toml index 4789914..f586abc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,9 +5,8 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html -[features] -serde = ["dep:serde"] [dependencies] -serde = { version = "1.0.197", features = ["derive"], optional = true } -zip = "0.6.6" +serde = { version = "1.0.197", features = ["derive"] } +serde_json = "1.0.114" +zip = { version = "0.6.6", default-features = false, features = ["deflate", "time"] } diff --git a/src/datapack/command.rs b/src/datapack/command.rs new file mode 100644 index 0000000..a3c431f --- /dev/null +++ b/src/datapack/command.rs @@ -0,0 +1,54 @@ +//! Represents a command that can be included in a function. + +use serde::{Deserialize, Serialize}; + +use crate::util::compile::{CompileOptions, MutCompilerState, MutFunctionCompilerState}; + +/// Represents a command that can be included in a function. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum Command { + /// A command that is already formatted as a string. + Raw(String), + /// Message to be printed only in debug mode + Debug(String), +} + +impl Command { + /// Create a new raw command. + pub fn raw(command: &str) -> Self { + Self::Raw(command.to_string()) + } + + /// Compile the command into a string. + pub fn compile( + &self, + options: &CompileOptions, + global_state: &MutCompilerState, + function_state: &MutFunctionCompilerState, + ) -> String { + let _ = options; + let _ = global_state; + let _ = function_state; + match self { + Self::Raw(command) => command.clone(), + Self::Debug(message) => compile_debug(message, options), + } + } +} + +impl From<&str> for Command { + fn from(command: &str) -> Self { + Self::raw(command) + } +} + +fn compile_debug(message: &str, option: &CompileOptions) -> String { + if option.debug { + 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"}}]"#, + message + ) + } else { + String::new() + } +} diff --git a/src/datapack/function.rs b/src/datapack/function.rs new file mode 100644 index 0000000..f81354d --- /dev/null +++ b/src/datapack/function.rs @@ -0,0 +1,48 @@ +//! Function struct and implementation + +use std::sync::Mutex; + +use serde::{Deserialize, Serialize}; + +use crate::{ + util::compile::{CompileOptions, FunctionCompilerState, MutCompilerState}, + virtual_fs::VFile, +}; + +use super::command::Command; + +/// Function that can be called by a command +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct Function { + commands: Vec, +} + +impl Function { + /// Create a new function. + pub fn new() -> Self { + Self::default() + } + + /// Add a command to the function. + pub fn add_command(&mut self, command: Command) { + self.commands.push(command); + } + + /// Get the commands of the function. + pub fn get_commands(&self) -> &Vec { + &self.commands + } + + /// Compile the function into a virtual file. + pub fn compile(&self, options: &CompileOptions, state: &MutCompilerState) -> VFile { + let function_state = Mutex::new(FunctionCompilerState::default()); + + let content = self + .commands + .iter() + .map(|c| c.compile(options, state, &function_state)) + .collect::>() + .join("\n"); + VFile::Text(content) + } +} diff --git a/src/datapack/mod.rs b/src/datapack/mod.rs new file mode 100644 index 0000000..935389d --- /dev/null +++ b/src/datapack/mod.rs @@ -0,0 +1,156 @@ +//! Datapack module for creating and managing Minecraft datapacks. + +mod command; +mod function; +mod namespace; +pub mod tag; +pub use command::Command; +pub use function::Function; +pub use namespace::Namespace; +use serde::{Deserialize, Serialize}; + +use std::{collections::HashMap, ops::RangeInclusive, path::Path, sync::Mutex}; + +use crate::{ + util::compile::{CompileOptions, CompilerState, MutCompilerState}, + virtual_fs::{VFile, VFolder}, +}; + +/// A Minecraft datapack. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Datapack { + // TODO: Support filter and overlays + name: String, + description: String, + pack_format: u8, + supported_formats: Option>, + namespaces: HashMap, + tick: Vec, + load: Vec, + custom_files: VFolder, +} + +impl Datapack { + /// Create a new Minecraft datapack. + pub fn new(name: &str, pack_format: u8) -> Self { + Self { + name: name.to_string(), + description: String::from("A Minecraft datapack created with shulkerbox"), + pack_format, + supported_formats: None, + namespaces: HashMap::new(), + tick: Vec::new(), + load: Vec::new(), + custom_files: VFolder::new(), + } + } + + /// Set the description of the datapack. + pub fn with_description(self, description: &str) -> Self { + Self { + description: description.to_string(), + ..self + } + } + + /// Set the supported pack formats of the datapack. + pub fn with_supported_formats(self, supported_formats: RangeInclusive) -> Self { + Self { + supported_formats: Some(supported_formats), + ..self + } + } + + /// Set the custom files of the datapack. + pub fn with_template_folder(self, path: &Path) -> std::io::Result { + let mut template = VFolder::try_from(path)?; + template.merge(self.custom_files); + + Ok(Self { + custom_files: template, + ..self + }) + } + + /// Add a namespace to the datapack. + pub fn add_namespace(&mut self, namespace: Namespace) { + if !namespace.get_main_function().get_commands().is_empty() { + self.add_tick(&format!("{}:main", namespace.get_name())); + } + self.namespaces + .insert(namespace.get_name().to_string(), namespace); + } + + /// Add a function to the tick function list. + pub fn add_tick(&mut self, function: &str) { + self.tick.push(function.to_string()); + } + + /// Add a function to the load function list. + pub fn add_load(&mut self, function: &str) { + self.load.push(function.to_string()); + } + + /// Add a custom file to the datapack. + pub fn add_custom_file(&mut self, path: &str, file: VFile) { + self.custom_files.add_file(path, file); + } + + /// Compile the pack into a virtual folder. + pub fn compile(&self, options: &CompileOptions) -> VFolder { + let compiler_state = Mutex::new(CompilerState::default()); + + let mut root_folder = self.custom_files.clone(); + let mcmeta = generate_mcmeta(self, options, &compiler_state); + root_folder.add_file("pack.mcmeta", mcmeta); + let mut data_folder = VFolder::new(); + + // Compile namespaces + for (name, namespace) in &self.namespaces { + let namespace_folder = namespace.compile(options, &compiler_state); + data_folder.add_existing_folder(name, namespace_folder); + } + + // Compile tick and load tag + if !self.tick.is_empty() { + let mut tick_tag = tag::Tag::new(tag::TagType::Functions, false); + for function in &self.tick { + tick_tag.add_value(tag::TagValue::Simple(function.to_owned())); + } + data_folder.add_file( + "minecraft/tags/functions/tick.json", + tick_tag.compile_no_state(options).1, + ); + } + if !self.load.is_empty() { + let mut load_tag = tag::Tag::new(tag::TagType::Functions, false); + for function in &self.tick { + load_tag.add_value(tag::TagValue::Simple(function.to_owned())); + } + data_folder.add_file( + "minecraft/tags/functions/load.json", + load_tag.compile_no_state(options).1, + ); + } + + root_folder.add_existing_folder("data", data_folder); + root_folder + } +} + +fn generate_mcmeta(dp: &Datapack, _options: &CompileOptions, _state: &MutCompilerState) -> VFile { + let mut content = serde_json::json!({ + "pack": { + "description": dp.description, + "pack_format": dp.pack_format + } + }); + if let Some(supported_formats) = &dp.supported_formats { + content["pack"]["supported_formats"] = serde_json::json!({ + "min_inclusive": *supported_formats.start(), + "max_inclusive": *supported_formats.end() + }) + } + + VFile::Text(content.to_string()) +} diff --git a/src/datapack/namespace.rs b/src/datapack/namespace.rs new file mode 100644 index 0000000..699e03e --- /dev/null +++ b/src/datapack/namespace.rs @@ -0,0 +1,93 @@ +//! Namespace of a datapack + +use serde::{Deserialize, Serialize}; + +use crate::{ + util::compile::{CompileOptions, MutCompilerState}, + virtual_fs::VFolder, +}; + +use super::{function::Function, tag::Tag}; +use std::collections::HashMap; + +/// Namespace of a datapack +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Namespace { + name: String, + functions: HashMap, + main_function: Function, + tags: HashMap, +} + +impl Namespace { + /// Create a new namespace. + pub fn new(name: &str) -> Self { + Self { + name: name.to_string(), + functions: HashMap::new(), + main_function: Function::default(), + tags: HashMap::new(), + } + } + + /// Get the name of the namespace. + pub fn get_name(&self) -> &str { + &self.name + } + + /// Get the main function of the namespace. + pub fn get_main_function(&self) -> &Function { + &self.main_function + } + /// Get the main function of the namespace mutably. + pub fn get_main_function_mut(&mut self) -> &mut Function { + &mut self.main_function + } + + /// Get the functions of the namespace. + pub fn get_functions(&self) -> &HashMap { + &self.functions + } + + /// Get the tags of the namespace. + pub fn get_tags(&self) -> &HashMap { + &self.tags + } + + /// Add a function to the namespace. + pub fn add_function(&mut self, name: &str, function: Function) { + self.functions.insert(name.to_string(), function); + } + + /// Add a tag to the namespace. + pub fn add_tag(&mut self, name: &str, tag: Tag) { + self.tags.insert(name.to_string(), tag); + } + + /// Compile the namespace into a virtual folder. + pub fn compile(&self, options: &CompileOptions, state: &MutCompilerState) -> VFolder { + let mut root_folder = VFolder::new(); + + // Compile functions + for (path, function) in &self.functions { + root_folder.add_file( + &format!("functions/{}.mcfunction", path), + function.compile(options, state), + ); + } + if !self.main_function.get_commands().is_empty() { + root_folder.add_file( + "functions/main.mcfunction", + self.main_function.compile(options, state), + ); + } + + // Compile tags + for (path, tag) in &self.tags { + let (tag_type, vfile) = tag.compile(options, state); + root_folder.add_file(&format!("tags/{}/{}.json", tag_type, path), vfile); + } + + root_folder + } +} diff --git a/src/datapack/tag.rs b/src/datapack/tag.rs new file mode 100644 index 0000000..a57ac36 --- /dev/null +++ b/src/datapack/tag.rs @@ -0,0 +1,114 @@ +//! A tag for various types. + +use serde::{Deserialize, Serialize}; + +use crate::{ + util::compile::{CompileOptions, MutCompilerState}, + virtual_fs::VFile, +}; + +/// A tag for various types. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Tag { + r#type: TagType, + replace: bool, + values: Vec, +} +impl Tag { + /// Create a new tag. + pub fn new(r#type: TagType, replace: bool) -> Self { + Self { + r#type, + replace, + values: Vec::new(), + } + } + + /// Add a value to the tag. + pub fn add_value(&mut self, value: TagValue) { + self.values.push(value); + } + + /// Compile the tag into a virtual file without state + pub fn compile_no_state(&self, _options: &CompileOptions) -> (String, VFile) { + let json = serde_json::json!({ + "replace": self.replace, + "values": self.values.iter().map(TagValue::compile).collect::>() + }); + let type_str = self.r#type.to_string(); + let vfile = VFile::Text(serde_json::to_string(&json).expect("Failed to serialize tag")); + + (type_str, vfile) + } + + /// Compile the tag into a virtual file. + pub fn compile(&self, options: &CompileOptions, _state: &MutCompilerState) -> (String, VFile) { + self.compile_no_state(options) + } +} + +/// The type of a tag. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum TagType { + /// A tag for blocks. + Blocks, + /// A tag for fluids. + Fluids, + /// A tag for items. + Items, + /// A tag for entities. + Entities, + /// A tag for game events. + GameEvents, + /// A tag for functions. + Functions, + /// A custom tag + /// `Others()` => `data//tags/` + Others(String), +} +impl ToString for TagType { + fn to_string(&self) -> String { + match self { + Self::Blocks => "blocks".to_string(), + Self::Fluids => "fluids".to_string(), + Self::Items => "items".to_string(), + Self::Entities => "entity_types".to_string(), + Self::GameEvents => "game_events".to_string(), + Self::Functions => "functions".to_string(), + Self::Others(path) => path.to_string(), + } + } +} + +/// The value of a tag. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum TagValue { + /// A simple value, either a resource location or an id of another tag. + Simple(String), + /// An advanced value, with an id (same as above) and whether the loading of the tag should fail when entry is not found. + Advanced { + /// The id of the tag. + id: String, + /// Whether the loading of the tag should fail when the entry is not found. + required: bool, + }, +} +impl From<&str> for TagValue { + fn from(value: &str) -> Self { + Self::Simple(value.to_string()) + } +} +impl TagValue { + /// Compile the tag value into a JSON value. + pub fn compile(&self) -> serde_json::Value { + match self { + Self::Simple(value) => serde_json::Value::String(value.clone()), + Self::Advanced { id, required } => { + let mut map = serde_json::Map::new(); + map.insert("id".to_string(), serde_json::Value::String(id.clone())); + map.insert("required".to_string(), serde_json::Value::Bool(*required)); + serde_json::Value::Object(map) + } + } + } +} diff --git a/src/lib.rs b/src/lib.rs index d5ae419..cacfa40 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,4 +10,6 @@ )] #![deny(unsafe_code)] +pub mod datapack; +pub mod util; pub mod virtual_fs; diff --git a/src/util/compile.rs b/src/util/compile.rs new file mode 100644 index 0000000..b1b38a5 --- /dev/null +++ b/src/util/compile.rs @@ -0,0 +1,30 @@ +//! Compile options for the compiler. + +use std::sync::Mutex; + +use serde::{Deserialize, Serialize}; + +/// Compile options for the compiler. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CompileOptions { + /// Whether to compile in debug mode. + pub debug: bool, +} + +impl Default for CompileOptions { + fn default() -> Self { + Self { debug: true } + } +} + +/// State of the compiler that can change during compilation. +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct CompilerState {} +/// Mutex for the compiler state. +pub type MutCompilerState = Mutex; + +/// State of the compiler for each function that can change during compilation. +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct FunctionCompilerState {} +/// Mutex for the function compiler state. +pub type MutFunctionCompilerState = Mutex; diff --git a/src/util/mod.rs b/src/util/mod.rs new file mode 100644 index 0000000..063a674 --- /dev/null +++ b/src/util/mod.rs @@ -0,0 +1,3 @@ +//! Utility functions for the Shulkerbox project. + +pub mod compile; diff --git a/src/virtual_fs.rs b/src/virtual_fs.rs index f4d4f9a..ff4980b 100644 --- a/src/virtual_fs.rs +++ b/src/virtual_fs.rs @@ -7,11 +7,11 @@ use std::{ path::Path, }; +use serde::{Deserialize, Serialize}; use zip::ZipWriter; /// Folder representation in virtual file system -#[derive(Debug, Default, Clone)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[derive(Debug, Default, Clone, Serialize, Deserialize)] pub struct VFolder { folders: HashMap, files: HashMap, @@ -185,6 +185,20 @@ impl VFolder { files } + + /// Recursively merge another folder into this folder. + pub fn merge(&mut self, other: Self) { + for (name, folder) in other.folders { + if let Some(existing_folder) = self.folders.get_mut(&name) { + existing_folder.merge(folder); + } else { + self.folders.insert(name, folder); + } + } + for (name, file) in other.files { + self.files.insert(name, file); + } + } } impl TryFrom<&Path> for VFolder { @@ -223,8 +237,7 @@ impl TryFrom<&Path> for VFolder { } /// File representation in virtual file system -#[derive(Debug, Clone)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[derive(Debug, Clone, Serialize, Deserialize)] pub enum VFile { /// Text file Text(String),