minecraft_command_types/datapack/
mod.rs

1pub mod pack;
2pub mod tag;
3
4use crate::datapack::pack::Pack;
5use crate::datapack::pack::feature::Features;
6use crate::datapack::pack::filter::Filter;
7use crate::datapack::pack::language::Language;
8use crate::datapack::pack::overlay::Overlays;
9use crate::datapack::tag::{Tag, TagType, Worldgen};
10use nonempty::NonEmpty;
11use serde::{Deserialize, Serialize};
12use serde_json::Value;
13use std::collections::BTreeMap;
14use std::path::Path;
15use std::{fs, io};
16
17#[derive(Debug, Clone, Serialize, Deserialize)]
18#[serde(rename_all = "snake_case")]
19pub struct PackMCMeta {
20    pub pack: Pack,
21    #[serde(skip_serializing_if = "Option::is_none")]
22    pub features: Option<Features>,
23    #[serde(skip_serializing_if = "Option::is_none")]
24    pub filter: Option<Filter>,
25    #[serde(skip_serializing_if = "Option::is_none")]
26    pub overlays: Option<Overlays>,
27    #[serde(skip_serializing_if = "Option::is_none")]
28    pub language: Option<BTreeMap<String, Language>>,
29}
30
31#[derive(Clone)]
32pub enum FilePathNode<T> {
33    Directory(String, Vec<FilePathNode<T>>),
34    File(String, T),
35}
36
37impl<T> FilePathNode<T> {
38    pub fn from_str(path: &str, value: T) -> Self {
39        let mut parts = path.split('/').rev();
40        let file_name = parts.next().expect("Path cannot be empty");
41        let mut current_node = FilePathNode::File(file_name.to_string(), value);
42
43        for part in parts {
44            current_node = FilePathNode::Directory(part.to_string(), vec![current_node]);
45        }
46
47        current_node
48    }
49
50    pub fn from_nonempty_vec_string(vec: &NonEmpty<String>, value: T) -> Self {
51        let mut vec = vec.iter().rev();
52
53        let file_name = vec.next().expect("Path cannot be empty");
54        let mut current_node = FilePathNode::File(file_name.to_string(), value);
55
56        for part in vec {
57            current_node = FilePathNode::Directory(part.to_string(), vec![current_node]);
58        }
59
60        current_node
61    }
62}
63
64#[derive(Clone, Default)]
65pub struct Namespace {
66    pub functions: Vec<FilePathNode<String>>,
67    pub tags: BTreeMap<TagType, Vec<FilePathNode<Tag>>>,
68
69    pub advancements: Vec<FilePathNode<Value>>,
70    pub banner_patterns: Vec<FilePathNode<Value>>,
71    pub cat_variants: Vec<FilePathNode<Value>>,
72    pub chat_types: Vec<FilePathNode<Value>>,
73    pub chicken_variants: Vec<FilePathNode<Value>>,
74    pub cow_variants: Vec<FilePathNode<Value>>,
75    pub damage_types: Vec<FilePathNode<Value>>,
76    pub dialogs: Vec<FilePathNode<Value>>,
77    pub dimensions: Vec<FilePathNode<Value>>,
78    pub dimension_types: Vec<FilePathNode<Value>>,
79    pub enchantments: Vec<FilePathNode<Value>>,
80    pub enchantment_providers: Vec<FilePathNode<Value>>,
81    pub frog_variants: Vec<FilePathNode<Value>>,
82    pub instruments: Vec<FilePathNode<Value>>,
83    pub item_modifiers: Vec<FilePathNode<Value>>,
84    pub jukebox_songs: Vec<FilePathNode<Value>>,
85    pub loot_tables: Vec<FilePathNode<Value>>,
86    pub painting_variants: Vec<FilePathNode<Value>>,
87    pub pig_variants: Vec<FilePathNode<Value>>,
88    pub predicates: Vec<FilePathNode<Value>>,
89    pub recipes: Vec<FilePathNode<Value>>,
90    pub test_environments: Vec<FilePathNode<Value>>,
91    pub test_instances: Vec<FilePathNode<Value>>,
92    pub timelines: Vec<FilePathNode<Value>>,
93    pub trial_spawners: Vec<FilePathNode<Value>>,
94    pub trim_materials: Vec<FilePathNode<Value>>,
95    pub trim_patterns: Vec<FilePathNode<Value>>,
96    pub wolf_sound_variants: Vec<FilePathNode<Value>>,
97    pub wolf_variants: Vec<FilePathNode<Value>>,
98    pub worldgen: Worldgen,
99}
100
101fn write_file_path_nodes<T>(
102    base_path: &Path,
103    nodes: &[FilePathNode<T>],
104    extension: &str,
105    serializer: &impl Fn(&T) -> io::Result<String>,
106) -> io::Result<()> {
107    fs::create_dir_all(base_path)?;
108    for node in nodes {
109        match node {
110            FilePathNode::Directory(name, children) => {
111                let dir_path = base_path.join(name);
112                write_file_path_nodes(&dir_path, children, extension, serializer)?;
113            }
114            FilePathNode::File(name, content) => {
115                let filename = if name.ends_with(extension) {
116                    name.clone()
117                } else {
118                    format!("{}{}", name, extension)
119                };
120                let file_path = base_path.join(filename);
121                let serialized_content = serializer(content)?;
122                fs::write(file_path, serialized_content)?;
123            }
124        }
125    }
126    Ok(())
127}
128
129impl Namespace {
130    pub fn write(&self, namespace_path: &Path) -> io::Result<()> {
131        let json_serializer = |v: &Value| serde_json::to_string_pretty(v).map_err(io::Error::other);
132
133        if !self.functions.is_empty() {
134            write_file_path_nodes(
135                &namespace_path.join("function"),
136                &self.functions,
137                ".mcfunction",
138                &|content_str| Ok(content_str.clone()),
139            )?;
140        }
141
142        if !self.tags.is_empty() {
143            let tags_root_path = namespace_path.join("tags");
144            for (tag_type, nodes) in &self.tags {
145                let type_path = if tag_type.is_worldgen() {
146                    tags_root_path.join("worldgen").join(tag_type.to_string())
147                } else {
148                    tags_root_path.join(tag_type.to_string())
149                };
150
151                write_file_path_nodes(&type_path, nodes, ".json", &|tag| {
152                    serde_json::to_string_pretty(tag).map_err(io::Error::other)
153                })?;
154            }
155        }
156
157        macro_rules! generate_write_file_path_nodes {
158            ($field_name:expr, $folder_name:expr) => {
159                if !$field_name.is_empty() {
160                    write_file_path_nodes(
161                        &namespace_path.join($folder_name),
162                        &$field_name,
163                        ".json",
164                        &json_serializer,
165                    )?;
166                }
167            };
168        }
169
170        generate_write_file_path_nodes!(self.advancements, "advancement");
171        generate_write_file_path_nodes!(self.banner_patterns, "banner_pattern");
172        generate_write_file_path_nodes!(self.cat_variants, "cat_variant");
173        generate_write_file_path_nodes!(self.chat_types, "chat_type");
174        generate_write_file_path_nodes!(self.chicken_variants, "chicken_variant");
175        generate_write_file_path_nodes!(self.cow_variants, "cow_variant");
176        generate_write_file_path_nodes!(self.damage_types, "damage_type");
177        generate_write_file_path_nodes!(self.dialogs, "dialog");
178        generate_write_file_path_nodes!(self.dimensions, "dimension");
179        generate_write_file_path_nodes!(self.dimension_types, "dimension_type");
180        generate_write_file_path_nodes!(self.enchantments, "enchantment");
181        generate_write_file_path_nodes!(self.enchantment_providers, "enchantment_provider");
182        generate_write_file_path_nodes!(self.frog_variants, "frog_variant");
183        generate_write_file_path_nodes!(self.instruments, "instrument");
184        generate_write_file_path_nodes!(self.item_modifiers, "item_modifier");
185        generate_write_file_path_nodes!(self.jukebox_songs, "jukebox_song");
186        generate_write_file_path_nodes!(self.loot_tables, "loot_table");
187        generate_write_file_path_nodes!(self.painting_variants, "painting_variant");
188        generate_write_file_path_nodes!(self.pig_variants, "pig_variant");
189        generate_write_file_path_nodes!(self.predicates, "predicate");
190        generate_write_file_path_nodes!(self.recipes, "recipe");
191        generate_write_file_path_nodes!(self.test_environments, "test_environment");
192        generate_write_file_path_nodes!(self.test_instances, "test_instance");
193        generate_write_file_path_nodes!(self.timelines, "timeline");
194        generate_write_file_path_nodes!(self.trial_spawners, "trial_spawner");
195        generate_write_file_path_nodes!(self.trim_materials, "trim_material");
196        generate_write_file_path_nodes!(self.trim_patterns, "trim_pattern");
197        generate_write_file_path_nodes!(self.wolf_sound_variants, "wolf_sound_variant");
198        generate_write_file_path_nodes!(self.wolf_variants, "wolf_variant");
199
200        Ok(())
201    }
202}
203
204pub struct Datapack {
205    pub pack: PackMCMeta,
206    pub namespaces: BTreeMap<String, Namespace>,
207}
208
209impl Datapack {
210    #[inline]
211    #[must_use]
212    pub fn new(pack_format: i32, description: Value) -> Datapack {
213        Datapack {
214            pack: PackMCMeta {
215                pack: Pack {
216                    pack_format: Some(pack_format),
217                    description,
218                    max_format: None,
219                    min_format: None,
220                    supported_formats: None,
221                },
222                features: None,
223                filter: None,
224                overlays: None,
225                language: None,
226            },
227            namespaces: BTreeMap::new(),
228        }
229    }
230    pub fn write(&self, datapack_directory: &Path) -> io::Result<()> {
231        fs::create_dir_all(datapack_directory)?;
232
233        let mcmeta_path = datapack_directory.join("pack.mcmeta");
234        let mcmeta_content = serde_json::to_string_pretty(&self.pack).map_err(io::Error::other)?;
235        fs::write(mcmeta_path, mcmeta_content)?;
236
237        let data_path = datapack_directory.join("data");
238
239        for (name, namespace) in &self.namespaces {
240            let namespace_path = data_path.join(name);
241            namespace.write(&namespace_path)?;
242        }
243
244        Ok(())
245    }
246
247    pub fn get_namespace_mut(&mut self, name: &str) -> &mut Namespace {
248        self.namespaces.entry(name.to_string()).or_default()
249    }
250
251    pub fn add_namespace(&mut self, name: String, namespace: Namespace) {
252        self.namespaces.insert(name, namespace);
253    }
254}