mcfunction_debug_adapter/adapter/
utils.rs1use 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() ;
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() .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 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) .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}