Skip to main content

cobble/transpiler/
data_pack.rs

1use crate::pack_format::{
2    PackFormat, COBBLE_VERSION, SUPPORTED_MINECRAFT_VERSION, SUPPORTED_PACK_FORMAT,
3};
4use crate::stdlib::{EventType, StdLib};
5use serde::{Deserialize, Serialize};
6use serde_json::json;
7use std::collections::{HashMap, HashSet};
8use std::fs;
9use std::io::Write;
10use std::path::{Path, PathBuf};
11
12fn stable_relative_path(path: &Path, root: &Path) -> PathBuf {
13    let canonical_path = path.canonicalize().unwrap_or_else(|_| path.to_path_buf());
14    let canonical_root = root.canonicalize().unwrap_or_else(|_| root.to_path_buf());
15
16    if let Ok(relative_path) = canonical_path.strip_prefix(&canonical_root) {
17        if !relative_path.as_os_str().is_empty() {
18            return relative_path.to_path_buf();
19        }
20    }
21
22    if path.is_relative() {
23        return path.to_path_buf();
24    }
25
26    canonical_path
27}
28
29#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
30pub struct SourceLocation {
31    pub file: PathBuf,
32    pub line: usize,
33    pub column: usize,
34}
35
36#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
37pub enum GeneratedCommandKind {
38    UserCommand,
39    StdLib,
40    RuntimeSetup,
41    ControlFlow,
42    JsonGenerated,
43}
44
45#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
46pub struct GeneratedCommand {
47    pub text: String,
48    pub source: Option<SourceLocation>,
49    pub kind: GeneratedCommandKind,
50}
51
52impl GeneratedCommand {
53    pub fn new(text: String, source: Option<SourceLocation>, kind: GeneratedCommandKind) -> Self {
54        Self { text, source, kind }
55    }
56}
57
58#[derive(Debug, Clone, Serialize, Deserialize)]
59pub struct SourceMapEntry {
60    pub generated_path: String,
61    pub generated_line: usize,
62    pub command: String,
63    pub source: Option<SourceLocation>,
64    pub kind: GeneratedCommandKind,
65}
66
67#[derive(Debug, Clone, Serialize, Deserialize)]
68pub struct SourceMap {
69    pub version: u8,
70    pub entries: Vec<SourceMapEntry>,
71}
72
73#[derive(Debug, Clone, Serialize)]
74pub struct BuildManifest {
75    pub version: u8,
76    pub cobble_version: String,
77    pub minecraft_version: String,
78    pub pack_format: PackFormat,
79    pub pack_format_text: String,
80    pub namespace: String,
81    pub description: String,
82    pub input: Option<BuildManifestInput>,
83    pub generated_namespaces: Vec<String>,
84    pub generated: BuildManifestGenerated,
85    pub resources: Vec<BuildManifestResourceEntry>,
86    pub validation: Option<BuildManifestValidation>,
87}
88
89#[derive(Debug, Clone, Serialize)]
90pub struct BuildManifestInput {
91    pub source: String,
92    pub entry_points: Vec<String>,
93    pub compiled_files: Vec<String>,
94}
95
96#[derive(Debug, Clone, Serialize)]
97pub struct BuildManifestGenerated {
98    pub functions: usize,
99    pub commands: usize,
100    pub source_map_entries: usize,
101    pub function_tags: usize,
102    pub stdlib_function_tags: usize,
103    pub custom_function_tags: usize,
104    pub json_function_tags: usize,
105    pub advancements: usize,
106    pub loot_tables: usize,
107    pub recipes: usize,
108    pub predicates: usize,
109    pub item_modifiers: usize,
110    pub json_resources: usize,
111    pub total_json_resources: usize,
112}
113
114#[derive(Debug, Clone, Serialize, PartialEq, Eq, Hash)]
115pub struct BuildManifestResourceEntry {
116    pub kind: String,
117    pub namespace: String,
118    pub path: String,
119}
120
121#[derive(Debug, Clone, Serialize)]
122pub struct BuildManifestValidation {
123    pub enabled: bool,
124    pub commands_json: String,
125    pub files_checked: usize,
126    pub commands_checked: usize,
127    pub macro_commands_checked: usize,
128    pub commands_skipped: usize,
129    pub errors: usize,
130    pub source_map_errors: usize,
131}
132
133pub struct DataPack {
134    pub namespace: String,
135    pub description: String,
136    pub output_dir: PathBuf,
137    pub functions: HashMap<String, Vec<String>>,
138    pub command_metadata: HashMap<String, HashMap<usize, GeneratedCommand>>,
139    pub tags: HashMap<String, Vec<String>>,
140    pub advancements: HashMap<String, String>,
141    pub loot_tables: HashMap<String, String>,
142    pub recipes: HashMap<String, String>,
143    pub predicates: HashMap<String, String>,
144    pub item_modifiers: HashMap<String, String>,
145    pub json_resources: HashMap<String, String>,
146    pub json_resource_origins: HashMap<String, SourceLocation>,
147    pub pack_format: PackFormat,
148    pub stdlib: StdLib,
149    pub used_objectives: HashSet<String>,
150    pub source_display_root: Option<PathBuf>,
151    pub build_input: Option<BuildManifestInput>,
152    pub validation_summary: Option<BuildManifestValidation>,
153}
154
155impl DataPack {
156    pub fn new(namespace: String, output_dir: PathBuf) -> Self {
157        Self {
158            namespace,
159            description: "Generated by Cobble".to_string(),
160            output_dir,
161            functions: HashMap::new(),
162            command_metadata: HashMap::new(),
163            tags: HashMap::new(),
164            advancements: HashMap::new(),
165            loot_tables: HashMap::new(),
166            recipes: HashMap::new(),
167            predicates: HashMap::new(),
168            item_modifiers: HashMap::new(),
169            json_resources: HashMap::new(),
170            json_resource_origins: HashMap::new(),
171            pack_format: SUPPORTED_PACK_FORMAT,
172            stdlib: StdLib::new(),
173            used_objectives: HashSet::new(),
174            source_display_root: None,
175            build_input: None,
176            validation_summary: None,
177        }
178    }
179
180    pub fn set_description(&mut self, desc: String) {
181        self.description = desc;
182    }
183
184    pub fn set_pack_format(&mut self, format: PackFormat) {
185        self.pack_format = format;
186    }
187
188    pub fn set_build_input(&mut self, input: BuildManifestInput) {
189        self.build_input = Some(input);
190    }
191
192    pub fn set_source_display_root(&mut self, root: PathBuf) {
193        self.source_display_root = Some(root);
194    }
195
196    pub fn set_validation_summary(&mut self, validation: Option<BuildManifestValidation>) {
197        self.validation_summary = validation;
198    }
199
200    pub fn generated_counts(&self) -> BuildManifestGenerated {
201        let resources = self.generated_resource_entries();
202        let source_map_entry_count = self.functions.values().map(Vec::len).sum();
203        self.generated_counts_with_source_map(source_map_entry_count, &resources)
204    }
205
206    pub fn build_manifest_snapshot(&self) -> BuildManifest {
207        let generated_namespaces = self.generated_namespaces();
208        let source_map_entry_count = self.functions.values().map(Vec::len).sum();
209        self.build_manifest(source_map_entry_count, &generated_namespaces)
210    }
211
212    fn metadata_for_commands(
213        commands: &[String],
214        kind: GeneratedCommandKind,
215    ) -> HashMap<usize, GeneratedCommand> {
216        commands
217            .iter()
218            .enumerate()
219            .map(|(index, command)| {
220                (
221                    index,
222                    GeneratedCommand::new(command.clone(), None, kind.clone()),
223                )
224            })
225            .collect()
226    }
227
228    fn complete_metadata(
229        commands: &[String],
230        mut metadata: HashMap<usize, GeneratedCommand>,
231        default_kind: GeneratedCommandKind,
232    ) -> HashMap<usize, GeneratedCommand> {
233        for (index, command) in commands.iter().enumerate() {
234            metadata.entry(index).or_insert_with(|| {
235                GeneratedCommand::new(command.clone(), None, default_kind.clone())
236            });
237        }
238        metadata
239    }
240
241    pub fn add_function(&mut self, name: String, commands: Vec<String>) {
242        self.add_function_with_kind(name, commands, GeneratedCommandKind::ControlFlow);
243    }
244
245    pub fn add_function_with_kind(
246        &mut self,
247        name: String,
248        commands: Vec<String>,
249        kind: GeneratedCommandKind,
250    ) {
251        let metadata = Self::metadata_for_commands(&commands, kind);
252        self.command_metadata.insert(name.clone(), metadata);
253        self.functions.insert(name, commands);
254    }
255
256    pub fn add_function_with_metadata(
257        &mut self,
258        name: String,
259        commands: Vec<String>,
260        metadata: HashMap<usize, GeneratedCommand>,
261    ) {
262        let metadata =
263            Self::complete_metadata(&commands, metadata, GeneratedCommandKind::ControlFlow);
264        self.command_metadata.insert(name.clone(), metadata);
265        self.functions.insert(name, commands);
266    }
267
268    pub fn insert_function_commands_with_kind(
269        &mut self,
270        name: &str,
271        index: usize,
272        new_commands: &[String],
273        kind: GeneratedCommandKind,
274    ) {
275        if new_commands.is_empty() {
276            return;
277        }
278
279        let Some(commands) = self.functions.get_mut(name) else {
280            return;
281        };
282        let insert_index = index.min(commands.len());
283        for (offset, command) in new_commands.iter().enumerate() {
284            commands.insert(insert_index + offset, command.clone());
285        }
286
287        let existing_metadata = self.command_metadata.remove(name).unwrap_or_default();
288        let mut updated_metadata = HashMap::new();
289        for (old_index, metadata) in existing_metadata {
290            let new_index = if old_index >= insert_index {
291                old_index + new_commands.len()
292            } else {
293                old_index
294            };
295            updated_metadata.insert(new_index, metadata);
296        }
297        for (offset, command) in new_commands.iter().enumerate() {
298            updated_metadata.insert(
299                insert_index + offset,
300                GeneratedCommand::new(command.clone(), None, kind.clone()),
301            );
302        }
303
304        let completed = Self::complete_metadata(
305            commands,
306            updated_metadata,
307            GeneratedCommandKind::ControlFlow,
308        );
309        self.command_metadata.insert(name.to_string(), completed);
310    }
311
312    pub fn insert_function_commands_with_metadata(
313        &mut self,
314        name: &str,
315        index: usize,
316        new_commands: &[String],
317        new_metadata: HashMap<usize, GeneratedCommand>,
318    ) {
319        if new_commands.is_empty() {
320            return;
321        }
322
323        let Some(commands) = self.functions.get_mut(name) else {
324            return;
325        };
326        let insert_index = index.min(commands.len());
327        for (offset, command) in new_commands.iter().enumerate() {
328            commands.insert(insert_index + offset, command.clone());
329        }
330
331        let existing_metadata = self.command_metadata.remove(name).unwrap_or_default();
332        let mut updated_metadata = HashMap::new();
333        for (old_index, metadata) in existing_metadata {
334            let new_index = if old_index >= insert_index {
335                old_index + new_commands.len()
336            } else {
337                old_index
338            };
339            updated_metadata.insert(new_index, metadata);
340        }
341
342        for (offset, command) in new_commands.iter().enumerate() {
343            let generated = new_metadata.get(&offset).cloned().unwrap_or_else(|| {
344                GeneratedCommand::new(command.clone(), None, GeneratedCommandKind::ControlFlow)
345            });
346            updated_metadata.insert(insert_index + offset, generated);
347        }
348
349        let completed = Self::complete_metadata(
350            commands,
351            updated_metadata,
352            GeneratedCommandKind::ControlFlow,
353        );
354        self.command_metadata.insert(name.to_string(), completed);
355    }
356
357    pub fn track_objective(&mut self, objective: &str) {
358        self.used_objectives.insert(objective.to_string());
359    }
360
361    pub fn ensure_init_function(&mut self) {
362        // Get or create the init function from load event handlers
363        let load_handlers = self.stdlib.get_event_handlers(&EventType::Load);
364
365        if let Some(init_func_name) = load_handlers.first().cloned() {
366            // Add objectives to the beginning of the load function
367            if let Some(commands) = self.functions.get_mut(&init_func_name) {
368                let mut setup_commands = Vec::new();
369
370                // Add gamerule first
371                let gamerule_cmd = "gamerule max_command_sequence_length 1000000000".to_string();
372                if !commands.contains(&gamerule_cmd) {
373                    setup_commands.push(gamerule_cmd);
374                }
375
376                // Then add objectives
377                let mut objectives: Vec<_> = self.used_objectives.iter().collect();
378                objectives.sort();
379                for objective in objectives {
380                    let obj_cmd = format!("scoreboard objectives add {} dummy", objective);
381                    // Only add if not already present
382                    if !commands.contains(&obj_cmd) {
383                        setup_commands.push(obj_cmd);
384                    }
385                }
386
387                // Initialize Boolean literal constants if __internal__ objective is used
388                if self.used_objectives.contains("__internal__") {
389                    let internal_init_1 =
390                        "scoreboard players set #true_const __internal__ 1".to_string();
391                    let internal_init_2 =
392                        "scoreboard players set #false_const __internal__ 0".to_string();
393                    if !commands.contains(&internal_init_1) {
394                        setup_commands.push(internal_init_1);
395                    }
396                    if !commands.contains(&internal_init_2) {
397                        setup_commands.push(internal_init_2);
398                    }
399                }
400
401                // Prepend setup commands to existing commands
402                if !setup_commands.is_empty() {
403                    let inserted_count = setup_commands.len();
404                    let mut updated_commands = setup_commands;
405                    updated_commands.extend(commands.clone());
406
407                    let mut updated_metadata = Self::metadata_for_commands(
408                        &updated_commands[..inserted_count],
409                        GeneratedCommandKind::RuntimeSetup,
410                    );
411                    if let Some(existing_metadata) = self.command_metadata.remove(&init_func_name) {
412                        for (index, command) in existing_metadata {
413                            updated_metadata.insert(index + inserted_count, command);
414                        }
415                    }
416                    updated_metadata = Self::complete_metadata(
417                        &updated_commands,
418                        updated_metadata,
419                        GeneratedCommandKind::ControlFlow,
420                    );
421
422                    self.command_metadata
423                        .insert(init_func_name.clone(), updated_metadata);
424                    *commands = updated_commands;
425                }
426            }
427        } else if !self.used_objectives.is_empty() {
428            // No load handler exists, create a default init function
429            let mut commands = Vec::new();
430
431            // Add gamerule first
432            commands.push("gamerule max_command_sequence_length 1000000000".to_string());
433
434            // Then add objectives
435            let mut objectives: Vec<_> = self.used_objectives.iter().collect();
436            objectives.sort();
437            for objective in objectives {
438                commands.push(format!("scoreboard objectives add {} dummy", objective));
439            }
440
441            // Initialize Boolean literal constants if __internal__ objective is used
442            if self.used_objectives.contains("__internal__") {
443                commands.push("scoreboard players set #true_const __internal__ 1".to_string());
444                commands.push("scoreboard players set #false_const __internal__ 0".to_string());
445            }
446
447            self.add_function_with_kind(
448                "_cobble_init".to_string(),
449                commands,
450                GeneratedCommandKind::RuntimeSetup,
451            );
452            self.stdlib
453                .add_event_listener(EventType::Load, "_cobble_init".to_string());
454        }
455    }
456
457    pub fn add_tag(&mut self, tag_name: String, functions: Vec<String>) {
458        self.tags.insert(tag_name, functions);
459    }
460
461    pub fn add_advancement(&mut self, name: String, json: String) {
462        self.advancements.insert(name, json);
463    }
464
465    pub fn add_loot_table(&mut self, name: String, json: String) {
466        self.loot_tables.insert(name, json);
467    }
468
469    pub fn add_recipe(&mut self, name: String, json: String) {
470        self.recipes.insert(name, json);
471    }
472
473    pub fn add_predicate(&mut self, name: String, json: String) {
474        self.predicates.insert(name, json);
475    }
476
477    pub fn add_item_modifier(&mut self, name: String, json: String) {
478        self.item_modifiers.insert(name, json);
479    }
480
481    pub fn add_json_resource(&mut self, relative_path: String, json: String) -> Result<(), String> {
482        self.add_json_resource_in_namespace(self.namespace.clone(), relative_path, json)
483    }
484
485    pub fn add_json_resource_in_namespace(
486        &mut self,
487        namespace: String,
488        relative_path: String,
489        json: String,
490    ) -> Result<(), String> {
491        self.add_json_resource_in_namespace_with_source(namespace, relative_path, json, None)
492    }
493
494    pub fn add_json_resource_in_namespace_with_source(
495        &mut self,
496        namespace: String,
497        relative_path: String,
498        json: String,
499        source: Option<SourceLocation>,
500    ) -> Result<(), String> {
501        let key = Self::json_resource_key(&namespace, &relative_path);
502        if let Some(existing_json) = self.json_resources.get(&key) {
503            let duplicate_kind =
504                Self::json_resource_duplicate_kind(&relative_path, existing_json, &json);
505            let mut message = format!(
506                "Duplicate data pack resource '{}:{}' ({})",
507                namespace, relative_path, duplicate_kind
508            );
509            if let Some(first_source) = self.json_resource_origins.get(&key) {
510                message.push_str(&format!(
511                    "\n  first declaration: {}",
512                    self.format_source_location(first_source)
513                ));
514            }
515            if let Some(second_source) = source.as_ref() {
516                message.push_str(&format!(
517                    "\n  second declaration: {}",
518                    self.format_source_location(second_source)
519                ));
520            }
521            return Err(message);
522        }
523        self.json_resources.insert(key.clone(), json);
524        if let Some(source) = source {
525            self.json_resource_origins.insert(key, source);
526        }
527        Ok(())
528    }
529
530    pub fn write(&self) -> std::io::Result<()> {
531        let data_dir = self.output_dir.join("data");
532        let namespace_dir = data_dir.join(&self.namespace);
533        let function_dir = namespace_dir.join("function");
534        let legacy_function_dir = namespace_dir.join("functions");
535        let minecraft_tags_dir = self
536            .output_dir
537            .join("data")
538            .join("minecraft")
539            .join("tags")
540            .join("function");
541        let legacy_minecraft_tags_dir = self
542            .output_dir
543            .join("data")
544            .join("minecraft")
545            .join("tags")
546            .join("functions");
547        let source_map_dir = self.output_dir.join(".cobble");
548        let generated_namespaces_path = source_map_dir.join("generated_namespaces.json");
549        let generated_namespaces = self.generated_namespaces();
550
551        if let Ok(content) = fs::read_to_string(&generated_namespaces_path) {
552            if let Ok(previous_namespaces) = serde_json::from_str::<Vec<String>>(&content) {
553                for namespace in previous_namespaces {
554                    if !Self::is_safe_namespace_path(&namespace) {
555                        continue;
556                    }
557
558                    if !generated_namespaces.contains(&namespace) {
559                        let old_namespace_dir = data_dir.join(namespace);
560                        if old_namespace_dir.exists() {
561                            fs::remove_dir_all(old_namespace_dir)?;
562                        }
563                    } else if namespace != self.namespace {
564                        Self::clean_generated_function_dirs(&data_dir.join(namespace))?;
565                    }
566                }
567            }
568        }
569
570        // Clean functions directory to remove stale .mcfunction files
571        // This prevents deleted or renamed functions from persisting in the datapack
572        if function_dir.exists() {
573            fs::remove_dir_all(&function_dir)?;
574        }
575        if legacy_function_dir.exists() {
576            fs::remove_dir_all(&legacy_function_dir)?;
577        }
578
579        // Clean generated metadata and JSON resource directories so rebuilds do
580        // not leave removed declarations behind in the output pack.
581        if source_map_dir.exists() {
582            fs::remove_dir_all(&source_map_dir)?;
583        }
584        if minecraft_tags_dir.exists() {
585            fs::remove_dir_all(&minecraft_tags_dir)?;
586        }
587        if legacy_minecraft_tags_dir.exists() {
588            fs::remove_dir_all(&legacy_minecraft_tags_dir)?;
589        }
590        for namespace in &generated_namespaces {
591            Self::clean_generated_resource_dirs(&data_dir.join(namespace))?;
592        }
593
594        fs::create_dir_all(&function_dir)?;
595
596        // Write pack.mcmeta
597        self.write_pack_mcmeta()?;
598
599        // Write all functions
600        let mut source_map_entries = Vec::new();
601        let mut function_names: Vec<_> = self.functions.keys().collect();
602        function_names.sort();
603        for name in function_names {
604            let commands = &self.functions[name];
605            let file_path = function_dir.join(format!("{}.mcfunction", name));
606            let mut file = fs::File::create(file_path)?;
607            let generated_path = format!("data/{}/function/{}.mcfunction", self.namespace, name);
608            for (index, command) in commands.iter().enumerate() {
609                writeln!(file, "{}", command)?;
610                let metadata = self
611                    .command_metadata
612                    .get(name)
613                    .and_then(|commands| commands.get(&index))
614                    .cloned()
615                    .unwrap_or_else(|| {
616                        GeneratedCommand::new(
617                            command.clone(),
618                            None,
619                            GeneratedCommandKind::ControlFlow,
620                        )
621                    });
622                source_map_entries.push(SourceMapEntry {
623                    generated_path: generated_path.clone(),
624                    generated_line: index + 1,
625                    command: metadata.text,
626                    source: metadata
627                        .source
628                        .map(|source| self.normalize_source_location(source)),
629                    kind: metadata.kind,
630                });
631            }
632        }
633
634        let source_map_entry_count = source_map_entries.len();
635        if !source_map_entries.is_empty() {
636            let source_map = SourceMap {
637                version: 1,
638                entries: source_map_entries,
639            };
640            fs::create_dir_all(&source_map_dir)?;
641            fs::write(
642                source_map_dir.join("source_map.json"),
643                serde_json::to_string_pretty(&source_map).unwrap(),
644            )?;
645        }
646        fs::create_dir_all(&source_map_dir)?;
647        fs::write(
648            source_map_dir.join("generated_namespaces.json"),
649            serde_json::to_string_pretty(&generated_namespaces).unwrap(),
650        )?;
651
652        let stdlib_tags = self.stdlib.generate_tags(&self.namespace);
653        let build_manifest = self.build_manifest(source_map_entry_count, &generated_namespaces);
654        fs::write(
655            source_map_dir.join("build_manifest.json"),
656            serde_json::to_string_pretty(&build_manifest).unwrap(),
657        )?;
658
659        // Write tags from stdlib
660        for (tag_name, functions) in stdlib_tags {
661            Self::write_function_tag(&data_dir, &self.namespace, &tag_name, &functions)?;
662        }
663        let mut custom_tag_names: Vec<_> = self.tags.keys().collect();
664        custom_tag_names.sort();
665        for tag_name in custom_tag_names {
666            Self::write_function_tag(&data_dir, &self.namespace, tag_name, &self.tags[tag_name])?;
667        }
668
669        // Write advancements
670        if !self.advancements.is_empty() {
671            let advancement_dir = namespace_dir.join("advancement");
672            Self::write_json_resource_map(&advancement_dir, &self.advancements)?;
673        }
674
675        // Write loot tables
676        if !self.loot_tables.is_empty() {
677            let loot_table_dir = namespace_dir.join("loot_table");
678            Self::write_json_resource_map(&loot_table_dir, &self.loot_tables)?;
679        }
680
681        // Write recipes
682        if !self.recipes.is_empty() {
683            let recipe_dir = namespace_dir.join("recipe");
684            Self::write_json_resource_map(&recipe_dir, &self.recipes)?;
685        }
686
687        // Write predicates
688        if !self.predicates.is_empty() {
689            let predicate_dir = namespace_dir.join("predicate");
690            Self::write_json_resource_map(&predicate_dir, &self.predicates)?;
691        }
692
693        // Write item modifiers
694        if !self.item_modifiers.is_empty() {
695            let item_modifier_dir = namespace_dir.join("item_modifier");
696            Self::write_json_resource_map(&item_modifier_dir, &self.item_modifiers)?;
697        }
698
699        // Write generic JSON resources generated by datapack.* declarations
700        let mut json_resource_paths: Vec<_> = self.json_resources.keys().collect();
701        json_resource_paths.sort();
702        for key in json_resource_paths {
703            let json = &self.json_resources[key];
704            let Some((resource_namespace, relative_path)) = Self::split_json_resource_key(key)
705            else {
706                continue;
707            };
708            let file_path = data_dir
709                .join(resource_namespace)
710                .join(format!("{}.json", relative_path));
711            if let Some(parent) = file_path.parent() {
712                fs::create_dir_all(parent)?;
713            }
714            let content = Self::merge_tag_json_if_existing(&file_path, relative_path, json)?;
715            fs::write(file_path, content)?;
716        }
717
718        Ok(())
719    }
720
721    fn build_manifest(
722        &self,
723        source_map_entry_count: usize,
724        generated_namespaces: &[String],
725    ) -> BuildManifest {
726        let resources = self.generated_resource_entries();
727        let generated = self.generated_counts_with_source_map(source_map_entry_count, &resources);
728
729        BuildManifest {
730            version: 1,
731            cobble_version: COBBLE_VERSION.to_string(),
732            minecraft_version: SUPPORTED_MINECRAFT_VERSION.to_string(),
733            pack_format: self.pack_format,
734            pack_format_text: self.pack_format.to_string(),
735            namespace: self.namespace.clone(),
736            description: self.description.clone(),
737            input: self.build_input.clone(),
738            generated_namespaces: generated_namespaces.to_vec(),
739            generated,
740            resources,
741            validation: self.validation_summary.clone(),
742        }
743    }
744
745    fn normalize_source_location(&self, mut source: SourceLocation) -> SourceLocation {
746        if let Some(root) = &self.source_display_root {
747            source.file = stable_relative_path(&source.file, root);
748        }
749        source
750    }
751
752    fn format_source_location(&self, source: &SourceLocation) -> String {
753        let source = self.normalize_source_location(source.clone());
754        format!(
755            "{}:{}:{}",
756            source.file.display(),
757            source.line,
758            source.column
759        )
760    }
761
762    fn json_resource_duplicate_kind(
763        relative_path: &str,
764        existing_json: &str,
765        new_json: &str,
766    ) -> &'static str {
767        if existing_json == new_json {
768            "exact duplicate"
769        } else if relative_path.starts_with("tags/") {
770            "invalid duplicate tag declaration"
771        } else {
772            "invalid overwrite"
773        }
774    }
775
776    fn generated_resource_entries(&self) -> Vec<BuildManifestResourceEntry> {
777        let mut entries = Vec::new();
778
779        entries.extend(self.stdlib_function_tag_entries());
780        entries.extend(self.custom_function_tag_entries());
781
782        entries.extend(
783            self.advancements
784                .keys()
785                .map(|name| self.resource_entry("advancement", name)),
786        );
787        entries.extend(
788            self.loot_tables
789                .keys()
790                .map(|name| self.resource_entry("loot_table", name)),
791        );
792        entries.extend(
793            self.recipes
794                .keys()
795                .map(|name| self.resource_entry("recipe", name)),
796        );
797        entries.extend(
798            self.predicates
799                .keys()
800                .map(|name| self.resource_entry("predicate", name)),
801        );
802        entries.extend(
803            self.item_modifiers
804                .keys()
805                .map(|name| self.resource_entry("item_modifier", name)),
806        );
807
808        for key in self.json_resources.keys() {
809            let Some((namespace, path)) = Self::split_json_resource_key(key) else {
810                continue;
811            };
812            entries.push(Self::resource_entry_from_json_path(namespace, path));
813        }
814
815        Self::sort_and_dedup_resource_entries(&mut entries);
816        entries
817    }
818
819    fn stdlib_function_tag_entries(&self) -> Vec<BuildManifestResourceEntry> {
820        let mut entries: Vec<_> = self
821            .stdlib
822            .generate_tags(&self.namespace)
823            .keys()
824            .map(|tag_name| {
825                Self::resource_entry_from_tag_name("function_tag", &self.namespace, tag_name)
826            })
827            .collect();
828        Self::sort_and_dedup_resource_entries(&mut entries);
829        entries
830    }
831
832    fn custom_function_tag_entries(&self) -> Vec<BuildManifestResourceEntry> {
833        let mut entries = Vec::new();
834
835        for tag_name in self.tags.keys() {
836            entries.push(Self::resource_entry_from_tag_name(
837                "function_tag",
838                &self.namespace,
839                tag_name,
840            ));
841        }
842
843        for key in self.json_resources.keys() {
844            let Some((namespace, path)) = Self::split_json_resource_key(key) else {
845                continue;
846            };
847            if let Some(path) = path.strip_prefix("tags/function/") {
848                entries.push(BuildManifestResourceEntry {
849                    kind: "function_tag".to_string(),
850                    namespace: namespace.to_string(),
851                    path: path.to_string(),
852                });
853            }
854        }
855
856        Self::sort_and_dedup_resource_entries(&mut entries);
857        entries
858    }
859
860    fn sort_and_dedup_resource_entries(entries: &mut Vec<BuildManifestResourceEntry>) {
861        entries.sort_by(|left, right| {
862            (
863                left.kind.as_str(),
864                left.namespace.as_str(),
865                left.path.as_str(),
866            )
867                .cmp(&(
868                    right.kind.as_str(),
869                    right.namespace.as_str(),
870                    right.path.as_str(),
871                ))
872        });
873        entries.dedup();
874    }
875
876    fn resource_entry(&self, kind: &str, path: &str) -> BuildManifestResourceEntry {
877        BuildManifestResourceEntry {
878            kind: kind.to_string(),
879            namespace: self.namespace.clone(),
880            path: path.to_string(),
881        }
882    }
883
884    fn resource_entry_from_tag_name(
885        kind: &str,
886        default_namespace: &str,
887        tag_name: &str,
888    ) -> BuildManifestResourceEntry {
889        let (namespace, path) = tag_name
890            .split_once(':')
891            .unwrap_or((default_namespace, tag_name));
892        BuildManifestResourceEntry {
893            kind: kind.to_string(),
894            namespace: namespace.to_string(),
895            path: path.to_string(),
896        }
897    }
898
899    fn resource_entry_from_json_path(namespace: &str, path: &str) -> BuildManifestResourceEntry {
900        let (kind, path) = if let Some(path) = path.strip_prefix("tags/function/") {
901            ("function_tag", path)
902        } else if let Some(path) = path.strip_prefix("tags/block/") {
903            ("block_tag", path)
904        } else if let Some(path) = path.strip_prefix("tags/item/") {
905            ("item_tag", path)
906        } else if let Some(path) = path.strip_prefix("tags/entity_type/") {
907            ("entity_type_tag", path)
908        } else if let Some(path) = path.strip_prefix("advancement/") {
909            ("advancement", path)
910        } else if let Some(path) = path.strip_prefix("loot_table/") {
911            ("loot_table", path)
912        } else if let Some(path) = path.strip_prefix("recipe/") {
913            ("recipe", path)
914        } else if let Some(path) = path.strip_prefix("predicate/") {
915            ("predicate", path)
916        } else if let Some(path) = path.strip_prefix("item_modifier/") {
917            ("item_modifier", path)
918        } else if let Some(path) = path.strip_prefix("dialog/") {
919            ("dialog", path)
920        } else {
921            ("json_resource", path)
922        };
923
924        BuildManifestResourceEntry {
925            kind: kind.to_string(),
926            namespace: namespace.to_string(),
927            path: path.to_string(),
928        }
929    }
930
931    fn generated_counts_with_source_map(
932        &self,
933        source_map_entry_count: usize,
934        resources: &[BuildManifestResourceEntry],
935    ) -> BuildManifestGenerated {
936        let mut json_advancement_count = 0;
937        let mut json_loot_table_count = 0;
938        let mut json_recipe_count = 0;
939        let mut json_predicate_count = 0;
940        let mut json_item_modifier_count = 0;
941        let mut json_function_tag_count = 0;
942
943        for key in self.json_resources.keys() {
944            let Some((_, path)) = Self::split_json_resource_key(key) else {
945                continue;
946            };
947            if path.starts_with("advancement/") {
948                json_advancement_count += 1;
949            } else if path.starts_with("loot_table/") {
950                json_loot_table_count += 1;
951            } else if path.starts_with("recipe/") {
952                json_recipe_count += 1;
953            } else if path.starts_with("predicate/") {
954                json_predicate_count += 1;
955            } else if path.starts_with("item_modifier/") {
956                json_item_modifier_count += 1;
957            } else if path.starts_with("tags/function/") {
958                json_function_tag_count += 1;
959            }
960        }
961
962        let advancement_count = self.advancements.len() + json_advancement_count;
963        let loot_table_count = self.loot_tables.len() + json_loot_table_count;
964        let recipe_count = self.recipes.len() + json_recipe_count;
965        let predicate_count = self.predicates.len() + json_predicate_count;
966        let item_modifier_count = self.item_modifiers.len() + json_item_modifier_count;
967        let legacy_typed_json_resources = self.advancements.len()
968            + self.loot_tables.len()
969            + self.recipes.len()
970            + self.predicates.len()
971            + self.item_modifiers.len();
972
973        let function_tag_count = resources
974            .iter()
975            .filter(|resource| resource.kind == "function_tag")
976            .count();
977        let stdlib_function_tags = self.stdlib_function_tag_entries();
978        let stdlib_function_tag_set: HashSet<_> = stdlib_function_tags.iter().cloned().collect();
979        let custom_function_tag_count = self
980            .custom_function_tag_entries()
981            .into_iter()
982            .filter(|entry| !stdlib_function_tag_set.contains(entry))
983            .count();
984
985        BuildManifestGenerated {
986            functions: self.functions.len(),
987            commands: self.functions.values().map(Vec::len).sum(),
988            source_map_entries: source_map_entry_count,
989            function_tags: function_tag_count,
990            stdlib_function_tags: stdlib_function_tags.len(),
991            custom_function_tags: custom_function_tag_count,
992            json_function_tags: json_function_tag_count,
993            advancements: advancement_count,
994            loot_tables: loot_table_count,
995            recipes: recipe_count,
996            predicates: predicate_count,
997            item_modifiers: item_modifier_count,
998            json_resources: self.json_resources.len(),
999            total_json_resources: legacy_typed_json_resources + self.json_resources.len(),
1000        }
1001    }
1002
1003    fn is_safe_namespace_path(namespace: &str) -> bool {
1004        !namespace.is_empty()
1005            && namespace.chars().all(|c| {
1006                c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_' || c == '-' || c == '.'
1007            })
1008    }
1009
1010    fn json_resource_key(namespace: &str, relative_path: &str) -> String {
1011        format!("{}/{}", namespace, relative_path)
1012    }
1013
1014    fn split_json_resource_key(key: &str) -> Option<(&str, &str)> {
1015        key.split_once('/')
1016    }
1017
1018    fn generated_namespaces(&self) -> Vec<String> {
1019        let mut namespaces = HashSet::new();
1020        namespaces.insert(self.namespace.clone());
1021        namespaces.insert("minecraft".to_string());
1022        for key in self.json_resources.keys() {
1023            if let Some((namespace, _)) = Self::split_json_resource_key(key) {
1024                namespaces.insert(namespace.to_string());
1025            }
1026        }
1027        for tag_name in self.tags.keys() {
1028            if let Some((namespace, _)) = tag_name.split_once(':') {
1029                namespaces.insert(namespace.to_string());
1030            }
1031        }
1032        let mut namespaces = namespaces.into_iter().collect::<Vec<_>>();
1033        namespaces.sort();
1034        namespaces
1035    }
1036
1037    fn clean_generated_resource_dirs(namespace_dir: &Path) -> std::io::Result<()> {
1038        let tags_dir = namespace_dir.join("tags");
1039        if tags_dir.exists() {
1040            fs::remove_dir_all(tags_dir)?;
1041        }
1042
1043        for resource_dir in [
1044            "advancement",
1045            "advancements",
1046            "loot_table",
1047            "loot_tables",
1048            "recipe",
1049            "recipes",
1050            "predicate",
1051            "predicates",
1052            "item_modifier",
1053            "item_modifiers",
1054            "dialog",
1055        ] {
1056            let path = namespace_dir.join(resource_dir);
1057            if path.exists() {
1058                fs::remove_dir_all(path)?;
1059            }
1060        }
1061        Ok(())
1062    }
1063
1064    fn clean_generated_function_dirs(namespace_dir: &Path) -> std::io::Result<()> {
1065        for function_dir in ["function", "functions"] {
1066            let path = namespace_dir.join(function_dir);
1067            if path.exists() {
1068                fs::remove_dir_all(path)?;
1069            }
1070        }
1071        Ok(())
1072    }
1073
1074    fn merge_tag_json_if_existing(
1075        file_path: &Path,
1076        relative_path: &str,
1077        new_json: &str,
1078    ) -> std::io::Result<String> {
1079        if !relative_path.starts_with("tags/") || !file_path.exists() {
1080            return Ok(new_json.to_string());
1081        }
1082
1083        let Ok(existing_content) = fs::read_to_string(file_path) else {
1084            return Ok(new_json.to_string());
1085        };
1086        let Ok(mut merged_value) = serde_json::from_str::<serde_json::Value>(&existing_content)
1087        else {
1088            return Ok(new_json.to_string());
1089        };
1090        let Ok(new_value) = serde_json::from_str::<serde_json::Value>(new_json) else {
1091            return Ok(new_json.to_string());
1092        };
1093
1094        let Some(merged_values) = merged_value
1095            .get_mut("values")
1096            .and_then(|value| value.as_array_mut())
1097        else {
1098            return Ok(new_json.to_string());
1099        };
1100        let Some(new_values) = new_value.get("values").and_then(|value| value.as_array()) else {
1101            return Ok(new_json.to_string());
1102        };
1103
1104        for new_value in new_values {
1105            if !merged_values.contains(new_value) {
1106                merged_values.push(new_value.clone());
1107            }
1108        }
1109
1110        serde_json::to_string_pretty(&merged_value)
1111            .map_err(|error| std::io::Error::new(std::io::ErrorKind::InvalidData, error))
1112    }
1113
1114    fn write_function_tag(
1115        data_dir: &Path,
1116        default_namespace: &str,
1117        tag_name: &str,
1118        functions: &[String],
1119    ) -> std::io::Result<()> {
1120        let (namespace, relative_tag_name) =
1121            if let Some((namespace, path)) = tag_name.split_once(':') {
1122                (namespace, path)
1123            } else {
1124                (default_namespace, tag_name)
1125            };
1126        let tag_file = data_dir
1127            .join(namespace)
1128            .join("tags")
1129            .join("function")
1130            .join(format!("{}.json", relative_tag_name));
1131
1132        let tag_content = json!({
1133            "values": functions
1134        });
1135
1136        if let Some(parent) = tag_file.parent() {
1137            fs::create_dir_all(parent)?;
1138        }
1139        let mut file = fs::File::create(tag_file)?;
1140        writeln!(
1141            file,
1142            "{}",
1143            serde_json::to_string_pretty(&tag_content).unwrap()
1144        )?;
1145        Ok(())
1146    }
1147
1148    fn write_json_resource_file(
1149        resource_dir: &Path,
1150        name: &str,
1151        json: &str,
1152    ) -> std::io::Result<()> {
1153        let file_path = resource_dir.join(format!("{}.json", name));
1154        if let Some(parent) = file_path.parent() {
1155            fs::create_dir_all(parent)?;
1156        }
1157        fs::write(file_path, json)
1158    }
1159
1160    fn write_json_resource_map(
1161        resource_dir: &Path,
1162        resources: &HashMap<String, String>,
1163    ) -> std::io::Result<()> {
1164        let mut names: Vec<_> = resources.keys().collect();
1165        names.sort();
1166        for name in names {
1167            Self::write_json_resource_file(resource_dir, name, &resources[name])?;
1168        }
1169        Ok(())
1170    }
1171
1172    fn write_pack_mcmeta(&self) -> std::io::Result<()> {
1173        let mcmeta_path = self.output_dir.join("pack.mcmeta");
1174
1175        let mcmeta_content = match self.pack_format {
1176            PackFormat::Decimal(major, minor) => {
1177                json!({
1178                    "pack": {
1179                        "description": self.description,
1180                        "min_format": [major, minor],
1181                        "max_format": [major, minor]
1182                    }
1183                })
1184            }
1185            PackFormat::Integer(v) => {
1186                json!({
1187                    "pack": {
1188                        "pack_format": v,
1189                        "description": self.description
1190                    }
1191                })
1192            }
1193        };
1194
1195        let mut file = fs::File::create(mcmeta_path)?;
1196        writeln!(
1197            file,
1198            "{}",
1199            serde_json::to_string_pretty(&mcmeta_content).unwrap()
1200        )?;
1201        Ok(())
1202    }
1203
1204    pub fn ensure_math_helper(&mut self, op: &str) {
1205        let func_name = format!("_cobble_math_{}", op);
1206        if self.functions.contains_key(&func_name) {
1207            return;
1208        }
1209
1210        let mut commands = Vec::new();
1211        match op {
1212            "abs" => {
1213                commands.push(
1214                    "scoreboard players operation #math_result math = #math_input math".to_string(),
1215                );
1216                commands.push("scoreboard players set #math_temp math -1".to_string());
1217                commands.push("execute if score #math_result math matches ..-1 run scoreboard players operation #math_result math *= #math_temp math".to_string());
1218            }
1219            "min" => {
1220                commands.push(
1221                    "scoreboard players operation #math_result math = #math_input math".to_string(),
1222                );
1223                commands.push("execute if score #math_input2 math < #math_result math run scoreboard players operation #math_result math = #math_input2 math".to_string());
1224            }
1225            "max" => {
1226                commands.push(
1227                    "scoreboard players operation #math_result math = #math_input math".to_string(),
1228                );
1229                commands.push("execute if score #math_input2 math > #math_result math run scoreboard players operation #math_result math = #math_input2 math".to_string());
1230            }
1231            "sqrt" => {
1232                commands.push("scoreboard players set #math_result math 0".to_string());
1233                commands.push(
1234                    "scoreboard players operation #sqrt_high math = #math_input math".to_string(),
1235                );
1236                commands.push("scoreboard players set #sqrt_low math 0".to_string());
1237                commands.push("scoreboard players set #sqrt_limit math 46340".to_string());
1238                commands.push("scoreboard players set #sqrt_two math 2".to_string());
1239                commands.push(
1240                    "execute if score #sqrt_high math matches ..-1 run scoreboard players set #sqrt_high math 0"
1241                        .to_string(),
1242                );
1243                commands.push(
1244                    "execute if score #sqrt_high math > #sqrt_limit math run scoreboard players operation #sqrt_high math = #sqrt_limit math"
1245                        .to_string(),
1246                );
1247
1248                for _ in 0..16 {
1249                    commands.push(
1250                        "scoreboard players operation #sqrt_mid math = #sqrt_low math".to_string(),
1251                    );
1252                    commands.push(
1253                        "scoreboard players operation #sqrt_mid math += #sqrt_high math"
1254                            .to_string(),
1255                    );
1256                    commands.push(
1257                        "scoreboard players operation #sqrt_mid math /= #sqrt_two math".to_string(),
1258                    );
1259                    commands.push(
1260                        "scoreboard players operation #sqrt_square math = #sqrt_mid math"
1261                            .to_string(),
1262                    );
1263                    commands.push(
1264                        "scoreboard players operation #sqrt_square math *= #sqrt_mid math"
1265                            .to_string(),
1266                    );
1267                    commands.push(
1268                        "execute if score #sqrt_square math <= #math_input math run scoreboard players operation #math_result math = #sqrt_mid math"
1269                            .to_string(),
1270                    );
1271                    commands.push(
1272                        "execute if score #sqrt_square math <= #math_input math run scoreboard players operation #sqrt_low math = #sqrt_mid math"
1273                            .to_string(),
1274                    );
1275                    commands.push(
1276                        "execute if score #sqrt_square math <= #math_input math run scoreboard players add #sqrt_low math 1"
1277                            .to_string(),
1278                    );
1279                    commands.push(
1280                        "execute if score #sqrt_square math > #math_input math run scoreboard players operation #sqrt_high math = #sqrt_mid math"
1281                            .to_string(),
1282                    );
1283                    commands.push(
1284                        "execute if score #sqrt_square math > #math_input math run scoreboard players remove #sqrt_high math 1"
1285                            .to_string(),
1286                    );
1287                }
1288            }
1289            _ => {}
1290        }
1291
1292        self.add_function_with_kind(func_name, commands, GeneratedCommandKind::StdLib);
1293    }
1294}