Basic implementation for datapack including functions and tags

This commit is contained in:
Moritz Hölting 2024-03-21 23:07:59 +01:00
parent 78a3d8f520
commit 2560588a17
10 changed files with 520 additions and 8 deletions

View File

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

54
src/datapack/command.rs Normal file
View File

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

48
src/datapack/function.rs Normal file
View File

@ -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<Command>,
}
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<Command> {
&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::<Vec<String>>()
.join("\n");
VFile::Text(content)
}
}

156
src/datapack/mod.rs Normal file
View File

@ -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<RangeInclusive<u8>>,
namespaces: HashMap<String, Namespace>,
tick: Vec<String>,
load: Vec<String>,
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<u8>) -> 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<Self> {
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())
}

93
src/datapack/namespace.rs Normal file
View File

@ -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<String, Function>,
main_function: Function,
tags: HashMap<String, Tag>,
}
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<String, Function> {
&self.functions
}
/// Get the tags of the namespace.
pub fn get_tags(&self) -> &HashMap<String, Tag> {
&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
}
}

114
src/datapack/tag.rs Normal file
View File

@ -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<TagValue>,
}
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::<Vec<_>>()
});
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(<registry path>)` => `data/<namespace>/tags/<registry path>`
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)
}
}
}
}

View File

@ -10,4 +10,6 @@
)]
#![deny(unsafe_code)]
pub mod datapack;
pub mod util;
pub mod virtual_fs;

30
src/util/compile.rs Normal file
View File

@ -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<CompilerState>;
/// 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<FunctionCompilerState>;

3
src/util/mod.rs Normal file
View File

@ -0,0 +1,3 @@
//! Utility functions for the Shulkerbox project.
pub mod compile;

View File

@ -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<String, VFolder>,
files: HashMap<String, VFile>,
@ -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),