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 let load_handlers = self.stdlib.get_event_handlers(&EventType::Load);
362
363 if let Some(init_func_name) = load_handlers.first().cloned() {
364 if let Some(commands) = self.functions.get_mut(&init_func_name) {
366 let mut setup_commands = Vec::new();
367
368 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 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 if !commands.contains(&obj_cmd) {
381 setup_commands.push(obj_cmd);
382 }
383 }
384
385 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 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 let mut commands = Vec::new();
428
429 commands.push("gamerule max_command_sequence_length 1000000000".to_string());
431
432 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 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 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 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 self.write_pack_mcmeta()?;
568
569 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 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 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 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 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 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 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 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}