mcfunction_debugger/adapter/
utils.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
19use crate::{
20    adapter::LISTENER_NAME,
21    generator::{
22        parser::command::resource_location::ResourceLocation, partition::SuspensionPositionInLine,
23    },
24};
25use debug_adapter_protocol::types::{Source, StackFrame};
26use futures::Stream;
27use minect::{
28    command::{QueryScoreboardOutput, SummonNamedEntityOutput},
29    log::LogEvent,
30};
31use std::{fmt::Display, path::Path, str::FromStr};
32use tokio_stream::StreamExt;
33
34pub fn parse_function_path(path: &Path) -> Result<(&Path, ResourceLocation), String> {
35    let datapack = find_parent_datapack(path).ok_or_else(|| {
36        format!(
37            "does not denote a path in a datapack directory with a pack.mcmeta file: {}",
38            &path.display()
39        )
40    })?;
41    let data_path = path.strip_prefix(datapack.join("data")).map_err(|_| {
42        format!(
43            "does not denote a path in the data directory of datapack {}: {}",
44            &datapack.display(),
45            &path.display()
46        )
47    })?;
48    let function = get_function_name(data_path, &path)?;
49    Ok((datapack, function))
50}
51
52pub fn find_parent_datapack(mut path: &Path) -> Option<&Path> {
53    while let Some(p) = path.parent() {
54        path = p;
55        let pack_mcmeta_path = path.join("pack.mcmeta");
56        if pack_mcmeta_path.is_file() {
57            return Some(path);
58        }
59    }
60    None
61}
62
63pub fn get_function_name(
64    data_path: impl AsRef<Path>,
65    path: impl AsRef<Path>,
66) -> Result<ResourceLocation, String> {
67    let namespace = data_path.as_ref()
68        .iter()
69        .next()
70        .ok_or_else(|| {
71            format!(
72                "contains an invalid path: {}",
73                path.as_ref().display()
74            )
75        })?
76        .to_str()
77        .unwrap() // Path is known to be UTF-8
78        ;
79    let fn_path = data_path
80        .as_ref()
81        .strip_prefix(Path::new(namespace).join("functions"))
82        .map_err(|_| format!("contains an invalid path: {}", path.as_ref().display()))?
83        .with_extension("")
84        .to_str()
85        .unwrap() // Path is known to be UTF-8
86        .replace(std::path::MAIN_SEPARATOR, "/");
87    Ok(ResourceLocation::new(&namespace, &fn_path))
88}
89
90pub(crate) fn events_between<'l>(
91    events: impl Stream<Item = LogEvent> + 'l,
92    start: &'l str,
93    stop: &'l str,
94) -> impl Stream<Item = LogEvent> + 'l {
95    events
96        .skip_while(move |event| !is_summon_output(event, start))
97        .skip(1) // Skip start tag
98        .take_while(move |event| !is_summon_output(event, stop))
99}
100fn is_summon_output(event: &LogEvent, name: &str) -> bool {
101    event.executor == LISTENER_NAME
102        && event
103            .output
104            .parse::<SummonNamedEntityOutput>()
105            .ok()
106            .filter(|output| output.name == name)
107            .is_some()
108}
109
110pub struct StoppedEvent {
111    pub function: ResourceLocation,
112    pub line_number: usize,
113    pub column_number: usize,
114    pub position_in_line: SuspensionPositionInLine,
115}
116impl FromStr for StoppedEvent {
117    type Err = ();
118
119    fn from_str(string: &str) -> Result<Self, Self::Err> {
120        fn from_str_inner(string: &str) -> Option<StoppedEvent> {
121            let string = string.strip_prefix("stopped+")?;
122            let (string, position_in_line) = string.rsplit_once('+')?;
123            let (string, column_number) = string.rsplit_once('+')?;
124            let (function, line_number) = string.rsplit_once('+')?;
125            Some(StoppedEvent {
126                function: parse_resource_location(function, '+')?,
127                line_number: line_number.parse().ok()?,
128                column_number: column_number.parse().ok()?,
129                position_in_line: position_in_line.parse().ok()?,
130            })
131        }
132        from_str_inner(string).ok_or(())
133    }
134}
135impl Display for StoppedEvent {
136    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
137        write!(
138            f,
139            "stopped+{}+{}+{}+{}",
140            write_resource_location(&self.function, '+'),
141            self.line_number,
142            self.column_number,
143            self.position_in_line,
144        )
145    }
146}
147
148pub(crate) struct StoppedData {
149    pub(crate) function: ResourceLocation,
150    pub(crate) line_number: usize,
151    pub(crate) position_in_line: SuspensionPositionInLine,
152    pub(crate) stack_trace: Vec<McfunctionStackFrame>,
153}
154
155pub(crate) struct McfunctionStackFrame {
156    pub(crate) id: i32,
157    pub(crate) location: SourceLocation,
158}
159impl McfunctionStackFrame {
160    pub(crate) fn to_stack_frame(
161        &self,
162        datapack: impl AsRef<Path>,
163        line_offset: usize,
164        column_offset: usize,
165    ) -> StackFrame {
166        let path = datapack
167            .as_ref()
168            .join("data")
169            .join(self.location.function.mcfunction_path())
170            .display()
171            .to_string();
172        StackFrame::builder()
173            .id(self.id)
174            .name(self.location.get_name())
175            .source(Some(Source::builder().path(Some(path)).build()))
176            .line((self.location.line_number - line_offset) as i32)
177            .column((self.location.column_number - column_offset) as i32)
178            .build()
179    }
180
181    pub(crate) fn parse(event: LogEvent) -> Option<McfunctionStackFrame> {
182        let location = event.executor.parse().ok()?;
183        let output = event.output.parse::<QueryScoreboardOutput>().ok()?;
184        let depth = output.score;
185        Some(McfunctionStackFrame {
186            id: depth,
187            location,
188        })
189    }
190}
191
192#[derive(Clone, Debug)]
193pub(crate) struct SourceLocation {
194    pub(crate) function: ResourceLocation,
195    pub(crate) line_number: usize,
196    pub(crate) column_number: usize,
197}
198impl FromStr for SourceLocation {
199    type Err = ();
200
201    fn from_str(string: &str) -> Result<Self, Self::Err> {
202        fn from_str_inner(string: &str) -> Option<SourceLocation> {
203            let has_column = 3 == string.bytes().filter(|b| *b == b':').count();
204            let (function_line_number, column_number) = if has_column {
205                let (function_line_number, column_number) = string.rsplit_once(':')?;
206                let column_number = column_number.parse().ok()?;
207                (function_line_number, column_number)
208            } else {
209                (string, 1)
210            };
211
212            let (function, line_number) = function_line_number.rsplit_once(':')?;
213            let function = parse_resource_location(function, ':')?;
214            let line_number = line_number.parse().ok()?;
215
216            Some(SourceLocation {
217                function,
218                line_number,
219                column_number,
220            })
221        }
222        from_str_inner(string).ok_or(())
223    }
224}
225impl SourceLocation {
226    pub(crate) fn get_name(&self) -> String {
227        format!("{}:{}", self.function, self.line_number)
228    }
229}
230
231fn parse_resource_location(function: &str, seperator: char) -> Option<ResourceLocation> {
232    if let [orig_ns, orig_fn @ ..] = function.split(seperator).collect::<Vec<_>>().as_slice() {
233        Some(ResourceLocation::new(orig_ns, &orig_fn.join("/")))
234    } else {
235        None
236    }
237}
238
239fn write_resource_location(function: &ResourceLocation, seperator: char) -> String {
240    format!(
241        "{}{}{}",
242        function.namespace(),
243        seperator,
244        function.path().replace("/", &seperator.to_string()),
245    )
246}