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 let load_handlers = self.stdlib.get_event_handlers(&EventType::Load);
364
365 if let Some(init_func_name) = load_handlers.first().cloned() {
366 if let Some(commands) = self.functions.get_mut(&init_func_name) {
368 let mut setup_commands = Vec::new();
369
370 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 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 if !commands.contains(&obj_cmd) {
383 setup_commands.push(obj_cmd);
384 }
385 }
386
387 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 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 let mut commands = Vec::new();
430
431 commands.push("gamerule max_command_sequence_length 1000000000".to_string());
433
434 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 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 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 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 self.write_pack_mcmeta()?;
598
599 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 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 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 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 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 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 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 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}