1pub mod config;
20pub mod parser;
21pub mod partition;
22mod template_engine;
23
24use crate::{
25 adapter::utils::StoppedEvent,
26 generator::{
27 config::GeneratorConfig,
28 parser::{
29 command::{
30 argument::minecraft::MinecraftEntityAnchor, resource_location::ResourceLocation,
31 CommandParser,
32 },
33 parse_line, Line,
34 },
35 partition::{partition, Partition, SuspensionPositionInLine, Terminator},
36 template_engine::{exclude_internal_entites_from_selectors, TemplateEngine},
37 },
38};
39use futures::{future::try_join_all, FutureExt};
40use multimap::MultiMap;
41use std::{
42 collections::{BTreeMap, BTreeSet, HashMap},
43 ffi::OsStr,
44 fs::read_to_string,
45 io::{self},
46 iter::FromIterator,
47 path::{Path, PathBuf},
48};
49use tokio::{
50 fs::{create_dir_all, write},
51 task::JoinHandle,
52 try_join,
53};
54use walkdir::WalkDir;
55
56pub struct DebugDatapackMetadata {
57 fn_ids: HashMap<ResourceLocation, usize>,
58}
59impl DebugDatapackMetadata {
60 pub fn new(fn_ids: HashMap<ResourceLocation, usize>) -> DebugDatapackMetadata {
61 DebugDatapackMetadata { fn_ids }
62 }
63
64 fn get_fn_score_holder(&self, fn_name: &ResourceLocation) -> String {
65 self.get_score_holder(fn_name, fn_name.to_string(), |id| format!("fn_{}", id))
66 }
67
68 pub fn get_breakpoint_score_holder(
69 &self,
70 fn_name: &ResourceLocation,
71 line_number: usize,
72 ) -> String {
73 self.get_score_holder(fn_name, format!("{}_{}", fn_name, line_number), |id| {
74 format!("fn_{}_{}", id, line_number)
75 })
76 }
77
78 fn get_score_holder(
79 &self,
80 fn_name: &ResourceLocation,
81 result: String,
82 fallback: impl Fn(&usize) -> String,
83 ) -> String {
84 const MAX_SCORE_HOLDER_LEN: usize = 40;
86 if result.len() <= MAX_SCORE_HOLDER_LEN {
87 result
88 } else if let Some(id) = self.fn_ids.get(fn_name) {
89 fallback(id)
90 } else {
91 result
94 }
95 }
96}
97
98pub async fn generate_debug_datapack<'l>(
100 input_path: impl AsRef<Path>,
101 output_path: impl AsRef<Path>,
102 config: &GeneratorConfig<'l>,
103) -> io::Result<DebugDatapackMetadata> {
104 let functions = find_function_files(input_path).await?;
105 let fn_ids = functions
106 .keys()
107 .enumerate()
108 .map(|(index, it)| (it.clone(), index))
109 .collect::<HashMap<_, _>>();
110 let metadata = DebugDatapackMetadata { fn_ids };
111
112 let fn_contents = parse_functions(&functions).await?;
113
114 let output_name = output_path
115 .as_ref()
116 .file_name()
117 .and_then(OsStr::to_str)
118 .unwrap_or_default();
119 let engine = TemplateEngine::new(
120 BTreeMap::from_iter([("-ns-", config.namespace), ("-datapack-", output_name)]),
121 config.adapter_listener_name,
122 );
123 expand_templates(&engine, &metadata, &fn_contents, &output_path).await?;
124
125 write_functions_txt(functions.keys(), &output_path).await?;
126
127 Ok(metadata)
128}
129
130async fn find_function_files(
131 datapack_path: impl AsRef<Path>,
132) -> Result<BTreeMap<ResourceLocation, PathBuf>, io::Error> {
133 let data_path = datapack_path.as_ref().join("data");
134 let threads = data_path
135 .read_dir()?
136 .collect::<io::Result<Vec<_>>>()?
137 .into_iter()
138 .map(|entry| get_functions(entry).map(|result| result?));
139
140 Ok(try_join_all(threads)
141 .await?
142 .into_iter()
143 .flat_map(|it| it)
144 .collect::<BTreeMap<ResourceLocation, PathBuf>>())
145}
146
147fn get_functions(
148 entry: std::fs::DirEntry,
149) -> JoinHandle<Result<Vec<(ResourceLocation, PathBuf)>, io::Error>> {
150 tokio::spawn(async move {
151 let mut functions = Vec::new();
152 if entry.file_type()?.is_dir() {
153 let namespace = entry.file_name();
154 let namespace_path = entry.path();
155 let functions_path = namespace_path.join("functions");
156 if functions_path.is_dir() {
157 for f_entry in WalkDir::new(&functions_path) {
158 let f_entry = f_entry?;
159 let path = f_entry.path().to_owned();
160 let file_type = f_entry.file_type();
161 if file_type.is_file() {
162 if let Some(extension) = path.extension() {
163 if extension == "mcfunction" {
164 let relative_path = path.strip_prefix(&functions_path).unwrap();
165 let name = ResourceLocation::new(
166 namespace.to_string_lossy().as_ref(),
167 &relative_path
168 .with_extension("")
169 .to_string_lossy()
170 .replace(std::path::MAIN_SEPARATOR, "/"),
171 );
172
173 functions.push((name, path));
174 }
175 }
176 }
177 }
178 }
179 }
180 Ok(functions)
181 })
182}
183
184async fn parse_functions<'l>(
185 functions: &'l BTreeMap<ResourceLocation, PathBuf>,
186) -> Result<HashMap<&'l ResourceLocation, Vec<(usize, String, Line)>>, io::Error> {
187 let parser =
188 CommandParser::default().map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
189 functions
190 .iter()
191 .map(|(name, path)| {
192 let lines = read_to_string(path)?
194 .split('\n')
195 .enumerate()
196 .map(|(line_index, line)| {
197 let line = line.strip_suffix('\r').unwrap_or(line); let command = parse_line(&parser, line);
199 (line_index + 1, line.to_string(), command)
200 })
201 .collect::<Vec<(usize, String, Line)>>();
202 Ok((name, lines))
203 })
204 .collect()
205}
206
207async fn expand_templates(
208 engine: &TemplateEngine<'_>,
209 metadata: &DebugDatapackMetadata,
210 fn_contents: &HashMap<&ResourceLocation, Vec<(usize, String, Line)>>,
211 output_path: impl AsRef<Path>,
212) -> io::Result<()> {
213 try_join!(
214 expand_global_templates(engine, metadata, fn_contents, &output_path),
215 expand_function_specific_templates(engine, metadata, fn_contents, &output_path),
216 )?;
217 Ok(())
218}
219
220macro_rules! expand_template {
221 ($e:expr, $o:expr, $p:expr) => {{
222 let path = $o.join($e.expand($p));
223 let content = $e.expand(include_template!($p));
224 write(path, content)
225 }};
226}
227
228async fn expand_global_templates(
229 engine: &TemplateEngine<'_>,
230 metadata: &DebugDatapackMetadata,
231 fn_contents: &HashMap<&ResourceLocation, Vec<(usize, String, Line)>>,
232 output_path: impl AsRef<Path>,
233) -> io::Result<()> {
234 let output_path = output_path.as_ref();
235
236 macro_rules! expand {
237 ($p:literal) => {
238 expand_template!(engine, output_path, $p)
239 };
240 }
241
242 try_join!(
243 create_dir_all(output_path.join(engine.expand("data/-ns-/functions/id"))),
244 create_dir_all(output_path.join("data/minecraft/tags/functions")),
245 )?;
246
247 try_join!(
248 expand!("data/-ns-/functions/id/assign.mcfunction"),
249 expand!("data/-ns-/functions/id/init_self.mcfunction"),
250 expand!("data/-ns-/functions/id/install.mcfunction"),
251 expand!("data/-ns-/functions/id/uninstall.mcfunction"),
252 expand!("data/-ns-/functions/abort_session.mcfunction"),
253 expand!("data/-ns-/functions/animate_context.mcfunction"),
254 expand!("data/-ns-/functions/decrement_age.mcfunction"),
255 expand!("data/-ns-/functions/freeze_aec.mcfunction"),
256 expand!("data/-ns-/functions/install_v1.mcfunction"),
257 expand!("data/-ns-/functions/install.mcfunction"),
258 expand!("data/-ns-/functions/kill_session.mcfunction"),
259 expand!("data/-ns-/functions/load.mcfunction"),
260 expand!("data/-ns-/functions/on_session_exit_successful.mcfunction"),
261 expand!("data/-ns-/functions/on_session_exit.mcfunction"),
262 expand!("data/-ns-/functions/prepare_resume.mcfunction"),
263 expand_schedule_template(&engine, fn_contents, &output_path),
264 expand!("data/-ns-/functions/select_entity.mcfunction"),
265 expand!("data/-ns-/functions/stop.mcfunction"),
266 expand!("data/-ns-/functions/tick_start.mcfunction"),
267 expand!("data/-ns-/functions/tick.mcfunction"),
268 expand!("data/-ns-/functions/unfreeze_aec.mcfunction"),
269 expand!("data/-ns-/functions/uninstall_v1.mcfunction"),
270 expand!("data/-ns-/functions/uninstall.mcfunction"),
271 expand_scores_templates(&engine, fn_contents, &output_path),
272 expand_validate_all_functions_template(&engine, metadata, fn_contents, &output_path),
273 expand!("data/minecraft/tags/functions/load.json"),
274 expand!("data/minecraft/tags/functions/tick.json"),
275 expand!("pack.mcmeta"),
276 )?;
277
278 Ok(())
279}
280
281async fn expand_schedule_template(
282 engine: &TemplateEngine<'_>,
283 fn_contents: &HashMap<&ResourceLocation, Vec<(usize, String, Line)>>,
284 output_path: impl AsRef<Path>,
285) -> io::Result<()> {
286 #[rustfmt::skip]
287 macro_rules! PATH { () => { "data/-ns-/functions/schedule.mcfunction" }; }
288
289 let content = fn_contents
290 .keys()
291 .map(|name| {
292 let engine = engine.extend_orig_name(name);
293 engine.expand(include_template!(PATH!()))
294 })
295 .collect::<Vec<_>>()
296 .join("");
297
298 let path = output_path.as_ref().join(engine.expand(PATH!()));
299 write(&path, &content).await
300}
301
302async fn expand_scores_templates(
303 engine: &TemplateEngine<'_>,
304 fn_contents: &HashMap<&ResourceLocation, Vec<(usize, String, Line)>>,
305 output_path: impl AsRef<Path>,
306) -> io::Result<()> {
307 let objectives = fn_contents
308 .values()
309 .flat_map(|vec| vec)
310 .filter_map(|(_, _, line)| line.objectives())
311 .flat_map(|objectives| objectives)
312 .collect::<BTreeSet<_>>();
313
314 expand_log_scores_template(&objectives, engine, &output_path).await?;
315
316 Ok(())
317}
318
319async fn expand_log_scores_template(
320 objectives: &BTreeSet<&String>,
321 engine: &TemplateEngine<'_>,
322 output_path: impl AsRef<Path>,
323) -> Result<(), io::Error> {
324 #[rustfmt::skip]
325 macro_rules! PATH { () => { "data/-ns-/functions/log_scores.mcfunction" }; }
326
327 let content = objectives
328 .iter()
329 .map(|objective| {
330 let engine = engine.extend([("-objective-", objective.as_str())]);
331 engine.expand(include_template!(PATH!()))
332 })
333 .collect::<Vec<_>>()
334 .join("");
335 let path = output_path.as_ref().join(engine.expand(PATH!()));
336 write(&path, &content).await
337}
338
339async fn expand_validate_all_functions_template(
340 engine: &TemplateEngine<'_>,
341 metadata: &DebugDatapackMetadata,
342 fn_contents: &HashMap<&ResourceLocation, Vec<(usize, String, Line)>>,
343 output_path: impl AsRef<Path>,
344) -> io::Result<()> {
345 #[rustfmt::skip]
346 macro_rules! PATH { () => { "data/-ns-/functions/validate_all_functions.mcfunction" }; }
347
348 let content = fn_contents
349 .keys()
350 .map(|name| {
351 let fn_score_holder = metadata.get_fn_score_holder(name);
352 let engine = engine
353 .extend_orig_name(name)
354 .extend([("-fn_score_holder-", fn_score_holder.as_str())]);
355 engine.expand(include_template!(PATH!()))
356 })
357 .collect::<Vec<_>>()
358 .join("");
359
360 let path = output_path.as_ref().join(engine.expand(PATH!()));
361 write(&path, &content).await
362}
363
364async fn expand_function_specific_templates(
365 engine: &TemplateEngine<'_>,
366 metadata: &DebugDatapackMetadata,
367 fn_contents: &HashMap<&ResourceLocation, Vec<(usize, String, Line)>>,
368 output_path: impl AsRef<Path>,
369) -> io::Result<()> {
370 let call_tree = create_call_tree(&fn_contents);
371
372 try_join_all(fn_contents.iter().map(|(fn_name, lines)| {
373 expand_function_templates(&engine, fn_name, lines, metadata, &call_tree, &output_path)
374 }))
375 .await?;
376
377 Ok(())
378}
379
380fn create_call_tree<'l>(
381 fn_contents: &'l HashMap<&ResourceLocation, Vec<(usize, String, Line)>>,
382) -> MultiMap<&'l ResourceLocation, (&'l ResourceLocation, &'l usize)> {
383 fn_contents
384 .iter()
385 .flat_map(|(&caller, lines)| {
386 lines
387 .iter()
388 .filter_map(move |(line_number, _line, command)| {
389 if let Line::FunctionCall { name: callee, .. } = command {
390 Some((callee, (caller, line_number)))
391 } else {
392 None
393 }
394 })
395 })
396 .collect()
397}
398
399async fn expand_function_templates(
400 engine: &TemplateEngine<'_>,
401 fn_name: &ResourceLocation,
402 lines: &Vec<(usize, String, Line)>,
403 metadata: &DebugDatapackMetadata,
404 call_tree: &MultiMap<&ResourceLocation, (&ResourceLocation, &usize)>,
405 output_path: impl AsRef<Path>,
406) -> io::Result<()> {
407 let fn_score_holder = metadata.get_fn_score_holder(fn_name);
408 let engine = engine
409 .extend_orig_name(fn_name)
410 .extend([("-fn_score_holder-", fn_score_holder.as_str())]);
411
412 let output_path = output_path.as_ref();
413 let fn_dir = output_path.join(engine.expand("data/-ns-/functions/-orig_ns-/-orig/fn-"));
414 create_dir_all(&fn_dir).await?;
415
416 let partitions = partition(lines);
417
418 let mut first = true;
419 for (partition_index, partition) in partitions.iter().enumerate() {
420 let position = partition.start.to_string();
421 let positions = format!("{}-{}", partition.start, partition.end);
422 let engine = engine.extend([
423 ("-position-", position.as_str()),
424 ("-positions-", positions.as_str()),
425 ]);
426 macro_rules! expand {
427 ($p:literal) => {
428 expand_template!(engine, output_path, $p)
429 };
430 }
431
432 if first {
433 expand!("data/-ns-/functions/-orig_ns-/-orig/fn-/next_iteration_or_return.mcfunction")
434 .await?;
435 first = false;
436 } else {
437 expand!(
438 "data/-ns-/functions/-orig_ns-/-orig/fn-/continue_current_iteration_at_-position-.mcfunction"
439 )
440 .await?;
441 }
442
443 #[rustfmt::skip]
445 macro_rules! PATH { () => {"data/-ns-/functions/-orig_ns-/-orig/fn-/continue_at_-position-.mcfunction"} }
446 let path = output_path.join(engine.expand(PATH!()));
447 let template = include_template!(PATH!()).to_string();
448 write(&path, &engine.expand(&template)).await?;
449
450 let mut content = partition
452 .regular_lines
453 .iter()
454 .map(|line| engine.expand_line(line, metadata))
455 .collect::<Vec<_>>()
456 .join("\n");
457
458 let terminator = match &partition.terminator {
459 Terminator::ConfigurableBreakpoint { position_in_line } => {
460 let column_index = match position_in_line {
461 SuspensionPositionInLine::Breakpoint => 0,
462 SuspensionPositionInLine::AfterFunction => {
463 let (_line_number, line, _parsed) = &lines[partition.end.line_number - 1];
464 line.len()
465 }
466 };
467 let next_partition = &partitions[partition_index + 1];
468 expand_terminator_suspend(
469 &engine,
470 output_path,
471 &metadata,
472 &fn_name,
473 partition.end.line_number,
474 *position_in_line,
475 column_index,
476 next_partition,
477 )
478 .await?
479 }
480 Terminator::FunctionCall {
481 column_index,
482 line,
483 name: called_fn,
484 anchor,
485 selectors,
486 } => {
487 let line_number = (partition.end.line_number).to_string();
488 let fn_score_holder = metadata.get_fn_score_holder(called_fn);
489 let execute = &line[..*column_index];
490 let execute = exclude_internal_entites_from_selectors(execute, selectors);
491 let debug_anchor = anchor.map_or("".to_string(), |anchor| {
492 let anchor_score = match anchor {
493 MinecraftEntityAnchor::Eyes => 1,
494 MinecraftEntityAnchor::Feet => 0,
495 };
496 format!(
497 "execute if score -fn_score_holder- -ns-_valid matches 1 run \
498 scoreboard players set current -ns-_anchor {anchor_score}",
499 anchor_score = anchor_score
500 )
501 });
502 let engine = engine.extend([
503 ("-line_number-", line_number.as_str()),
504 ("-call_ns-", called_fn.namespace()),
505 ("-call/fn-", called_fn.path()),
506 ("-fn_score_holder-", fn_score_holder.as_str()),
507 ("execute run ", &execute),
508 ("# -debug_anchor-", &debug_anchor),
509 ]);
510 engine.expand(include_template!(
511 "data/template/functions/terminator_function_call.mcfunction"
512 ))
513 }
514 Terminator::Return => engine.expand(include_template!(
515 "data/template/functions/terminator_return.mcfunction"
516 )),
517 };
518 content.push('\n');
519 content.push_str(&terminator);
520
521 expand_template!(
522 engine.extend([("# -content-", content.as_str())]),
523 output_path,
524 "data/-ns-/functions/-orig_ns-/-orig/fn-/-positions-.mcfunction"
525 )
526 .await?;
527 }
528
529 macro_rules! expand {
530 ($p:literal) => {
531 expand_template!(engine, output_path, $p)
532 };
533 }
534
535 try_join!(
536 expand!("data/-ns-/functions/-orig_ns-/-orig/fn-/return_or_exit.mcfunction"),
537 expand!("data/-ns-/functions/-orig_ns-/-orig/fn-/return.mcfunction"),
538 expand!("data/-ns-/functions/-orig_ns-/-orig/fn-/scheduled.mcfunction"),
539 expand!("data/-ns-/functions/-orig_ns-/-orig/fn-/start_valid.mcfunction"),
540 expand!("data/-ns-/functions/-orig_ns-/-orig/fn-/start.mcfunction"),
541 )?;
542
543 if let Some(callers) = call_tree.get_vec(fn_name) {
544 let mut return_cases = callers
545 .iter()
546 .map(|(caller, line_number)| {
547 engine.expand(&format!(
548 "execute if entity \
549 @s[tag=-ns-+{caller_ns}+{caller_fn_tag}+{line_number}] run \
550 function -ns-:{caller_ns}/{caller_fn}/\
551 continue_current_iteration_at_{line_number}_function",
552 caller_ns = caller.namespace(),
553 caller_fn = caller.path(),
554 caller_fn_tag = caller.path().replace("/", "+"),
555 line_number = line_number
556 ))
557 })
558 .collect::<Vec<_>>();
559 return_cases.sort();
560 let return_cases = return_cases.join("\n");
561
562 expand_template!(
563 engine.extend([("# -return_cases-", return_cases.as_str())]),
564 output_path,
565 "data/-ns-/functions/-orig_ns-/-orig/fn-/return_self.mcfunction"
566 )
567 .await?;
568 }
569
570 let commands = lines
571 .iter()
572 .map(|(_, line, parsed)| match parsed {
573 Line::Empty | Line::Comment => line.to_string(),
574 _ => {
575 format!(
576 "execute if score 1 -ns-_constant matches 0 run {}",
577 line.trim_start()
578 )
579 }
580 })
581 .collect::<Vec<_>>()
582 .join("\n");
583 expand_template!(
584 engine.extend([("# -commands-", commands.as_str())]),
585 output_path,
586 "data/-ns-/functions/-orig_ns-/-orig/fn-/validate.mcfunction"
587 )
588 .await?;
589
590 Ok(())
591}
592
593async fn expand_terminator_suspend(
594 engine: &TemplateEngine<'_>,
595 output_path: &Path,
596 metadata: &DebugDatapackMetadata,
597 fn_name: &ResourceLocation,
598 line_number: usize,
599 position_in_line: SuspensionPositionInLine,
600 column_index: usize,
601 next_partition: &Partition<'_>,
602) -> io::Result<String> {
603 let position_str = format!("{}_{}", line_number, position_in_line,);
604 {
605 let stopped_event = StoppedEvent {
606 function: fn_name.clone(),
607 line_number,
608 column_number: column_index + 1,
609 position_in_line,
610 };
611 let stopped_event_str = stopped_event.to_string();
612 let engine = engine.extend([
613 ("-position-", position_str.as_str()),
614 ("-stopped_event-", stopped_event_str.as_str()),
615 ]);
616 expand_template!(
617 engine,
618 output_path,
619 "data/-ns-/functions/-orig_ns-/-orig/fn-/suspend_at_-position-.mcfunction"
620 )
621 .await?;
622 }
623 let mut result = String::new();
624 if position_in_line == SuspensionPositionInLine::Breakpoint {
625 let score_holder = metadata.get_breakpoint_score_holder(fn_name, line_number);
626 let engine = engine.extend([("-score_holder-", score_holder.as_str())]);
627 result.push_str(&engine.expand(include_template!(
628 "data/template/functions/terminator_breakpoint_check.mcfunction"
629 )));
630 }
631 {
632 let next_positions = format!("{}-{}", next_partition.start, next_partition.end);
633 let engine = engine.extend([
634 ("-position-", position_str.as_str()),
635 ("-next_positions-", next_positions.as_str()),
636 ]);
637 result.push_str(&engine.expand(include_template!(
638 "data/template/functions/terminator_suspend.mcfunction"
639 )));
640 }
641 Ok(result)
642}
643
644async fn write_functions_txt(
645 fn_names: impl IntoIterator<Item = &ResourceLocation>,
646 output_path: impl AsRef<Path>,
647) -> io::Result<()> {
648 let path = output_path.as_ref().join("functions.txt");
649 let content = fn_names
650 .into_iter()
651 .map(|it| it.to_string())
652 .collect::<Vec<_>>()
653 .join("\n");
654 write(&path, content).await?;
655
656 Ok(())
657}