mcfunction_debug_adapter/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-2023 Adrodoc <adrodoc55@googlemail.com> & skess42 <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::{MinecraftSession, LISTENER_NAME},
21    error::PartialErrorResponse,
22};
23use debug_adapter_protocol::{
24    events::StoppedEventReason,
25    types::{Source, StackFrame},
26};
27use futures::Stream;
28use mcfunction_debugger::{
29    config::{
30        adapter::{
31            AdapterConfig, BreakpointKind, BreakpointPositionInLine, LocalBreakpoint,
32            LocalBreakpointPosition,
33        },
34        Config,
35    },
36    generate_debug_datapack,
37    parser::command::resource_location::ResourceLocation,
38    StoppedReason,
39};
40use minect::{command::SummonNamedEntityOutput, log::LogEvent};
41use multimap::MultiMap;
42use std::{fmt::Display, path::Path, str::FromStr};
43use tokio::fs::remove_dir_all;
44use tokio_stream::StreamExt;
45
46pub fn parse_function_path(path: &Path) -> Result<(&Path, ResourceLocation), String> {
47    let datapack = find_parent_datapack(path).ok_or_else(|| {
48        format!(
49            "does not denote a path in a datapack directory with a pack.mcmeta file: {}",
50            &path.display()
51        )
52    })?;
53    let data_path = path.strip_prefix(datapack.join("data")).map_err(|_| {
54        format!(
55            "does not denote a path in the data directory of datapack {}: {}",
56            &datapack.display(),
57            &path.display()
58        )
59    })?;
60    let function = get_function_name(data_path, &path)?;
61    Ok((datapack, function))
62}
63
64pub fn find_parent_datapack(mut path: &Path) -> Option<&Path> {
65    while let Some(p) = path.parent() {
66        path = p;
67        let pack_mcmeta_path = path.join("pack.mcmeta");
68        if pack_mcmeta_path.is_file() {
69            return Some(path);
70        }
71    }
72    None
73}
74
75pub fn get_function_name(
76    data_path: impl AsRef<Path>,
77    path: impl AsRef<Path>,
78) -> Result<ResourceLocation, String> {
79    let namespace = data_path.as_ref()
80        .iter()
81        .next()
82        .ok_or_else(|| {
83            format!(
84                "contains an invalid path: {}",
85                path.as_ref().display()
86            )
87        })?
88        .to_str()
89        .unwrap() // Path is known to be UTF-8
90        ;
91    let fn_path = data_path
92        .as_ref()
93        .strip_prefix(Path::new(namespace).join("functions"))
94        .map_err(|_| format!("contains an invalid path: {}", path.as_ref().display()))?
95        .with_extension("")
96        .to_str()
97        .unwrap() // Path is known to be UTF-8
98        .replace(std::path::MAIN_SEPARATOR, "/");
99    Ok(ResourceLocation::new(&namespace, &fn_path))
100}
101
102pub(super) async fn generate_datapack(
103    minecraft_session: &MinecraftSession,
104    breakpoints: &MultiMap<ResourceLocation, LocalBreakpoint>,
105    temporary_breakpoints: &MultiMap<ResourceLocation, LocalBreakpoint>,
106) -> Result<(), PartialErrorResponse> {
107    let mut breakpoints = breakpoints.clone();
108
109    // Add all generated breakpoints that are not at the same position as user breakpoints
110    for (key, values) in temporary_breakpoints.iter_all() {
111        for value in values {
112            if !contains_breakpoint(
113                &breakpoints,
114                &BreakpointPosition::from_breakpoint(key.clone(), &value.position),
115            ) {
116                breakpoints.insert(key.clone(), value.clone());
117            }
118        }
119    }
120
121    let config = Config {
122        namespace: &minecraft_session.namespace,
123        shadow: false,
124        adapter: Some(AdapterConfig {
125            adapter_listener_name: LISTENER_NAME,
126            breakpoints: &breakpoints,
127        }),
128    };
129    let _ = remove_dir_all(&minecraft_session.output_path).await;
130    generate_debug_datapack(
131        &minecraft_session.datapack,
132        &minecraft_session.output_path,
133        &config,
134    )
135    .await
136    .map_err(|e| PartialErrorResponse::new(format!("Failed to generate debug datapack: {}", e)))?;
137    Ok(())
138}
139
140pub(crate) fn can_resume_from(
141    breakpoints: &MultiMap<ResourceLocation, LocalBreakpoint>,
142    position: &BreakpointPosition,
143) -> bool {
144    get_breakpoint_kind(breakpoints, position)
145        .map(|it| it.can_resume())
146        .unwrap_or(false)
147}
148
149pub(crate) fn contains_breakpoint(
150    breakpoints: &MultiMap<ResourceLocation, LocalBreakpoint>,
151    position: &BreakpointPosition,
152) -> bool {
153    get_breakpoint_kind(breakpoints, position).is_some()
154}
155
156pub(crate) fn get_breakpoint_kind<'l>(
157    breakpoints: &'l MultiMap<ResourceLocation, LocalBreakpoint>,
158    position: &BreakpointPosition,
159) -> Option<&'l BreakpointKind> {
160    if let Some(breakpoints) = breakpoints.get_vec(&position.function) {
161        breakpoints
162            .iter()
163            .filter(|it| it.position.line_number == position.line_number)
164            .filter(|it| it.position.position_in_line == position.position_in_line)
165            .map(|it| &it.kind)
166            .next()
167    } else {
168        None
169    }
170}
171
172pub(crate) fn events_between<'l>(
173    events: impl Stream<Item = LogEvent> + 'l,
174    start: &'l str,
175    stop: &'l str,
176) -> impl Stream<Item = LogEvent> + 'l {
177    events
178        .skip_while(move |event| !is_summon_output(event, start))
179        .skip(1) // Skip start tag
180        .take_while(move |event| !is_summon_output(event, stop))
181}
182fn is_summon_output(event: &LogEvent, name: &str) -> bool {
183    event.executor == LISTENER_NAME
184        && event
185            .output
186            .parse::<SummonNamedEntityOutput>()
187            .ok()
188            .filter(|output| output.name == name)
189            .is_some()
190}
191
192#[derive(Clone, Debug, Eq, PartialEq)]
193pub(crate) struct BreakpointPosition {
194    pub(crate) function: ResourceLocation,
195    pub(crate) line_number: usize,
196    pub(crate) position_in_line: BreakpointPositionInLine,
197}
198impl BreakpointPosition {
199    pub(crate) fn from_breakpoint(
200        function: ResourceLocation,
201        position: &LocalBreakpointPosition,
202    ) -> BreakpointPosition {
203        BreakpointPosition {
204            function,
205            line_number: position.line_number,
206            position_in_line: position.position_in_line,
207        }
208    }
209}
210impl FromStr for BreakpointPosition {
211    type Err = ();
212
213    fn from_str(string: &str) -> Result<Self, Self::Err> {
214        fn from_str_inner(string: &str) -> Option<BreakpointPosition> {
215            let (function, position) = string.rsplit_once('+')?;
216            let (line_number, position_in_line) = position.split_once('_')?;
217
218            let function = parse_resource_location(function, '+')?;
219            let line_number = line_number.parse().ok()?;
220            let position_in_line = position_in_line.parse().ok()?;
221
222            Some(BreakpointPosition {
223                function,
224                line_number,
225                position_in_line,
226            })
227        }
228        from_str_inner(string).ok_or(())
229    }
230}
231impl Display for BreakpointPosition {
232    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
233        write!(
234            f,
235            "{}+{}+{}_{}",
236            self.function.namespace(),
237            self.function.path().replace("/", "+"),
238            self.line_number,
239            self.position_in_line,
240        )
241    }
242}
243
244pub(crate) struct StoppedData {
245    pub(crate) position: BreakpointPosition,
246    pub(crate) stack_trace: Vec<McfunctionStackFrame>,
247}
248
249pub(crate) struct StoppedEvent {
250    pub(crate) reason: StoppedReason,
251    pub(crate) position: BreakpointPosition,
252}
253impl FromStr for StoppedEvent {
254    type Err = ();
255
256    fn from_str(string: &str) -> Result<Self, Self::Err> {
257        fn from_str_inner(string: &str) -> Option<StoppedEvent> {
258            let string = string.strip_prefix("stopped+")?;
259            let (reason, position) = string.split_once('+')?;
260            let reason = reason.parse().ok()?;
261            let position = position.parse().ok()?;
262            Some(StoppedEvent { reason, position })
263        }
264        from_str_inner(string).ok_or(())
265    }
266}
267pub(crate) fn to_stopped_event_reason(reason: StoppedReason) -> StoppedEventReason {
268    match reason {
269        StoppedReason::Breakpoint => StoppedEventReason::Breakpoint,
270        StoppedReason::Step => StoppedEventReason::Step,
271    }
272}
273
274pub(crate) struct McfunctionStackFrame {
275    pub(crate) id: i32,
276    pub(crate) location: SourceLocation,
277}
278impl McfunctionStackFrame {
279    pub(crate) fn to_stack_frame(
280        &self,
281        datapack: impl AsRef<Path>,
282        line_offset: usize,
283        column_offset: usize,
284    ) -> StackFrame {
285        let path = datapack
286            .as_ref()
287            .join("data")
288            .join(self.location.function.mcfunction_path())
289            .display()
290            .to_string();
291        StackFrame::builder()
292            .id(self.id)
293            .name(self.location.get_name())
294            .source(Some(Source::builder().path(Some(path)).build()))
295            .line((self.location.line_number - line_offset) as i32)
296            .column((self.location.column_number - column_offset) as i32)
297            .build()
298    }
299}
300
301#[derive(Clone, Debug)]
302pub(crate) struct SourceLocation {
303    pub(crate) function: ResourceLocation,
304    pub(crate) line_number: usize,
305    pub(crate) column_number: usize,
306}
307impl FromStr for SourceLocation {
308    type Err = ();
309
310    fn from_str(string: &str) -> Result<Self, Self::Err> {
311        fn from_str_inner(string: &str) -> Option<SourceLocation> {
312            let has_column = 3 == string.bytes().filter(|b| *b == b':').count();
313            let (function_line_number, column_number) = if has_column {
314                let (function_line_number, column_number) = string.rsplit_once(':')?;
315                let column_number = column_number.parse().ok()?;
316                (function_line_number, column_number)
317            } else {
318                (string, 1)
319            };
320
321            let (function, line_number) = function_line_number.rsplit_once(':')?;
322            let function = parse_resource_location(function, ':')?;
323            let line_number = line_number.parse().ok()?;
324
325            Some(SourceLocation {
326                function,
327                line_number,
328                column_number,
329            })
330        }
331        from_str_inner(string).ok_or(())
332    }
333}
334impl SourceLocation {
335    pub(crate) fn get_name(&self) -> String {
336        format!("{}:{}", self.function, self.line_number)
337    }
338}
339
340fn parse_resource_location(function: &str, seperator: char) -> Option<ResourceLocation> {
341    if let [orig_ns, orig_fn @ ..] = function.split(seperator).collect::<Vec<_>>().as_slice() {
342        Some(ResourceLocation::new(orig_ns, &orig_fn.join("/")))
343    } else {
344        None
345    }
346}