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