mcfunction_debugger/
generator.rs

1// Mcfunction-Debugger is a debugger for Minecraft's *.mcfunction files that does not require any
2// Minecraft mods.
3//
4// © Copyright (C) 2021-2024 Adrodoc <adrodoc55@googlemail.com> & Skagaros <skagaros@gmail.com>
5//
6// This file is part of Mcfunction-Debugger.
7//
8// Mcfunction-Debugger is free software: you can redistribute it and/or modify it under the terms of
9// the GNU General Public License as published by the Free Software Foundation, either version 3 of
10// the License, or (at your option) any later version.
11//
12// Mcfunction-Debugger is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
13// without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14// GNU General Public License for more details.
15//
16// You should have received a copy of the GNU General Public License along with Mcfunction-Debugger.
17// If not, see <http://www.gnu.org/licenses/>.
18
19pub 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        /// Before Minecraft 1.18 score holder names can't be longer than 40 characters
85        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            // If this is a missing function, it is ok to use the whole name, even if it is to long.
92            // In this case the -ns-_valid score can not be set, but it is not set for missing functions anyways.
93            result
94        }
95    }
96}
97
98/// Visible for testing only. This is a binary crate, it is not intended to be used as a library.
99pub 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            // TODO async
193            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); // Remove trailing carriage return on Windows
198                    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        // continue_at_-position-.mcfunction
444        #[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        // -positions-.mcfunction
451        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}