1pub mod utils;
20
21use crate::{
22 adapter::utils::{
23 can_resume_from, events_between, generate_datapack, parse_function_path,
24 to_stopped_event_reason, BreakpointPosition, McfunctionStackFrame, StoppedData,
25 StoppedEvent,
26 },
27 error::{PartialErrorResponse, RequestError},
28 installer::establish_connection,
29 DebugAdapter, DebugAdapterContext,
30};
31use async_trait::async_trait;
32use debug_adapter_protocol::{
33 events::{Event, OutputCategory, OutputEventBody, StoppedEventBody, TerminatedEventBody},
34 requests::{
35 ContinueRequestArguments, EvaluateRequestArguments, InitializeRequestArguments,
36 LaunchRequestArguments, NextRequestArguments, PathFormat, PauseRequestArguments,
37 ScopesRequestArguments, SetBreakpointsRequestArguments, StackTraceRequestArguments,
38 StepInRequestArguments, StepOutRequestArguments, TerminateRequestArguments,
39 VariablesRequestArguments,
40 },
41 responses::{
42 ContinueResponseBody, EvaluateResponseBody, ScopesResponseBody, SetBreakpointsResponseBody,
43 StackTraceResponseBody, ThreadsResponseBody, VariablesResponseBody,
44 },
45 types::{Breakpoint, Capabilities, Scope, Thread, Variable},
46 ProtocolMessage,
47};
48use futures::future::Either;
49use log::trace;
50use mcfunction_debugger::{
51 config::adapter::{
52 BreakpointKind, BreakpointPositionInLine, LocalBreakpoint, LocalBreakpointPosition,
53 },
54 parser::{
55 command::{resource_location::ResourceLocation, CommandParser},
56 parse_line, Line,
57 },
58};
59use minect::{
60 command::{
61 enable_logging_command, logged_command, named_logged_command, query_scoreboard_command,
62 reset_logging_command, summon_named_entity_command, AddTagOutput, QueryScoreboardOutput,
63 SummonNamedEntityOutput,
64 },
65 log::LogEvent,
66 Command, MinecraftConnection,
67};
68use multimap::MultiMap;
69use std::{
70 convert::TryFrom,
71 io,
72 path::{Path, PathBuf},
73};
74use tokio::{
75 fs::{read_to_string, remove_dir_all, File},
76 io::{AsyncBufReadExt, BufReader},
77 sync::mpsc::UnboundedSender,
78};
79use tokio_stream::{wrappers::LinesStream, StreamExt};
80
81const LISTENER_NAME: &'static str = "mcfunction_debugger";
82
83struct ClientSession {
84 lines_start_at_1: bool,
85 columns_start_at_1: bool,
86 path_format: PathFormat,
87 minecraft_session: Option<MinecraftSession>,
88 breakpoints: MultiMap<ResourceLocation, LocalBreakpoint>,
89 temporary_breakpoints: MultiMap<ResourceLocation, LocalBreakpoint>,
90 parser: CommandParser,
91}
92impl ClientSession {
93 fn get_line_offset(&self) -> usize {
94 if self.lines_start_at_1 {
95 0
96 } else {
97 1
98 }
99 }
100
101 fn get_column_offset(&self) -> usize {
102 if self.columns_start_at_1 {
103 0
104 } else {
105 1
106 }
107 }
108}
109
110struct MinecraftSession {
111 connection: MinecraftConnection,
112 datapack: PathBuf,
113 namespace: String,
114 output_path: PathBuf,
115 scopes: Vec<ScopeReference>,
116 stopped_data: Option<StoppedData>,
117}
118impl MinecraftSession {
119 fn get_function_path(&self, function: &ResourceLocation) -> PathBuf {
120 self.datapack.join("data").join(function.mcfunction_path())
121 }
122
123 fn new_step_breakpoint(
124 &self,
125 function: ResourceLocation,
126 line_number: usize,
127 position_in_line: BreakpointPositionInLine,
128 depth: usize,
129 ) -> (ResourceLocation, LocalBreakpoint) {
130 let condition = self.replace_ns(&format!("if score current -ns-_depth matches {}", depth));
131 let kind = BreakpointKind::Step { condition };
132 let position = LocalBreakpointPosition {
133 line_number,
134 position_in_line,
135 };
136 (function, LocalBreakpoint { kind, position })
137 }
138
139 async fn create_step_in_breakpoints(
140 &self,
141 stack_trace: &[McfunctionStackFrame],
142 parser: &CommandParser,
143 ) -> Result<Vec<(ResourceLocation, LocalBreakpoint)>, RequestError<io::Error>> {
144 let mut breakpoints = Vec::new();
145
146 if stack_trace.is_empty() {
147 return Ok(breakpoints); }
149 let current = &stack_trace[0];
150 let current_depth = stack_trace.len() - 1;
151 let current_path = self.get_function_path(¤t.location.function);
152
153 let callee =
154 get_function_command(current_path, current.location.line_number, &parser).await?;
155 if let Some(callee) = callee {
156 let callee_path = self.get_function_path(&callee);
157 let callee_line_number = find_first_target_line_number(&callee_path, &parser).await?;
158
159 breakpoints.push(self.new_step_breakpoint(
160 callee,
161 callee_line_number,
162 BreakpointPositionInLine::Breakpoint,
163 current_depth + 1,
164 ));
165 }
166
167 breakpoints.extend(
168 self.create_step_over_breakpoints(&stack_trace, &parser)
169 .await?,
170 );
171
172 Ok(breakpoints)
173 }
174
175 async fn create_step_over_breakpoints(
176 &self,
177 stack_trace: &[McfunctionStackFrame],
178 parser: &CommandParser,
179 ) -> Result<Vec<(ResourceLocation, LocalBreakpoint)>, RequestError<io::Error>> {
180 let mut breakpoints = Vec::new();
181
182 if stack_trace.is_empty() {
183 return Ok(breakpoints); }
185 let current = &stack_trace[0];
186 let current_depth = stack_trace.len() - 1;
187 let current_path = self.get_function_path(¤t.location.function);
188
189 let next_line_number = find_step_target_line_number(
190 ¤t_path,
191 current.location.line_number,
192 &parser,
193 false,
194 )
195 .await?;
196 if let Some(next_line_number) = next_line_number {
197 breakpoints.push(self.new_step_breakpoint(
198 current.location.function.clone(),
199 next_line_number,
200 BreakpointPositionInLine::Breakpoint,
201 current_depth,
202 ));
203 } else {
204 breakpoints.extend(
205 self.create_step_out_breakpoint(&stack_trace, &parser)
206 .await?,
207 );
208
209 let first_line_number = find_first_target_line_number(¤t_path, &parser).await?;
211 breakpoints.push(self.new_step_breakpoint(
212 current.location.function.clone(),
213 first_line_number,
214 BreakpointPositionInLine::Breakpoint,
215 current_depth,
216 ));
217 }
218
219 Ok(breakpoints)
220 }
221
222 async fn create_step_out_breakpoint(
223 &self,
224 stack_trace: &[McfunctionStackFrame],
225 parser: &CommandParser,
226 ) -> Result<Vec<(ResourceLocation, LocalBreakpoint)>, RequestError<io::Error>> {
227 let mut breakpoints = Vec::new();
228
229 if stack_trace.len() <= 1 {
230 return Ok(breakpoints);
231 }
232 let caller = &stack_trace[1];
233
234 let line_number = find_step_target_line_number(
235 self.get_function_path(&caller.location.function),
236 caller.location.line_number,
237 parser,
238 true,
239 )
240 .await?;
241
242 let position_in_line = if line_number.is_some() {
243 BreakpointPositionInLine::Breakpoint
244 } else {
245 BreakpointPositionInLine::AfterFunction
246 };
247
248 let current_depth = stack_trace.len() - 1;
249 let caller_depth = current_depth - 1;
250 breakpoints.push(self.new_step_breakpoint(
251 caller.location.function.clone(),
252 line_number.unwrap_or(caller.location.line_number),
253 position_in_line,
254 caller_depth,
255 ));
256
257 Ok(breakpoints)
258 }
259
260 fn inject_commands(&mut self, commands: Vec<Command>) -> Result<(), PartialErrorResponse> {
261 inject_commands(&mut self.connection, commands)
262 .map_err(|e| PartialErrorResponse::new(format!("Failed to inject commands: {}", e)))
263 }
264
265 fn replace_ns(&self, command: &str) -> String {
266 command.replace("-ns-", &self.namespace)
267 }
268
269 async fn get_context_entity_id(&mut self, depth: i32) -> Result<i32, PartialErrorResponse> {
270 let events = self.connection.add_listener();
271
272 const START: &str = "get_context_entity_id.start";
273 const END: &str = "get_context_entity_id.end";
274
275 let scoreboard = self.replace_ns("-ns-_id");
276 self.inject_commands(vec![
277 Command::named(LISTENER_NAME, summon_named_entity_command(START)),
278 Command::new(query_scoreboard_command(
279 self.replace_ns(&format!(
280 "@e[\
281 type=area_effect_cloud,\
282 tag=-ns-_context,\
283 tag=-ns-_active,\
284 tag=-ns-_current,\
285 scores={{-ns-_depth={}}},\
286 ]",
287 depth
288 )),
289 &scoreboard,
290 )),
291 Command::named(LISTENER_NAME, summon_named_entity_command(END)),
292 ])?;
293
294 events_between(events, START, END)
295 .filter_map(|event| event.output.parse::<QueryScoreboardOutput>().ok())
296 .filter(|output| output.scoreboard == scoreboard)
297 .map(|output| output.score)
298 .next()
299 .await
300 .ok_or_else(|| PartialErrorResponse::new("Minecraft connection closed".to_string()))
301 }
302
303 fn get_cached_stack_trace(
304 &self,
305 ) -> Result<&Vec<McfunctionStackFrame>, RequestError<io::Error>> {
306 let stack_trace = &self
307 .stopped_data
308 .as_ref()
309 .ok_or(PartialErrorResponse::new("Not stopped".to_string()))?
310 .stack_trace;
311 Ok(stack_trace)
312 }
313
314 async fn get_stack_trace(&mut self) -> io::Result<Vec<McfunctionStackFrame>> {
315 const START: &str = "stack_trace.start";
316 const END: &str = "stack_trace.end";
317 let stack_trace_tag = self.replace_ns("-ns-_stack_trace");
318 let depth_scoreboard = self.replace_ns("-ns-_depth");
319
320 let events = self.connection.add_listener();
321
322 let commands = vec![
323 Command::named(LISTENER_NAME, summon_named_entity_command(START)),
324 Command::new(self.replace_ns(&format!(
325 "execute as @e[type=area_effect_cloud,tag=-ns-_function_call] run {}",
326 query_scoreboard_command("@s", &depth_scoreboard)
327 ))),
328 Command::new(self.replace_ns(&format!(
329 "execute as @e[type=area_effect_cloud,tag=-ns-_breakpoint] run tag @s add {}",
330 stack_trace_tag
331 ))),
332 Command::new(self.replace_ns(&format!(
333 "execute as @e[type=area_effect_cloud,tag=-ns-_breakpoint] run tag @s remove {}",
334 stack_trace_tag
335 ))),
336 Command::named(LISTENER_NAME, summon_named_entity_command(END)),
337 ];
338 inject_commands(&mut self.connection, commands)?;
339
340 let mut stack_trace = Vec::new();
341 let mut events = events_between(events, START, END);
342 while let Some(event) = events.next().await {
343 if let Ok(location) = event.executor.parse() {
344 let id = if let Some(output) = event
345 .output
346 .parse::<QueryScoreboardOutput>()
347 .ok()
348 .filter(|output| output.scoreboard == depth_scoreboard)
349 {
350 output.score } else if let Some(_) = event
352 .output
353 .parse::<AddTagOutput>()
354 .ok()
355 .filter(|output| output.tag == stack_trace_tag)
356 {
357 stack_trace.len() as i32 } else {
359 continue; };
361 stack_trace.push(McfunctionStackFrame { id, location });
362 }
363 }
364 stack_trace.sort_by_key(|it| -it.id);
365 Ok(stack_trace)
366 }
367
368 async fn uninstall_datapack(&mut self) -> io::Result<()> {
369 let events = self.connection.add_listener();
370
371 let uninstalled = format!("{}.uninstalled", LISTENER_NAME);
372 inject_commands(
373 &mut self.connection,
374 vec![
375 Command::new("function debug:uninstall"),
376 Command::new(summon_named_entity_command(&uninstalled)),
377 ],
378 )?;
379
380 trace!("Waiting for datapack to be uninstalled...");
381 events
382 .filter_map(|e| e.output.parse::<SummonNamedEntityOutput>().ok())
383 .filter(|o| o.name == uninstalled)
384 .next()
385 .await;
386 trace!("Datapack is uninstalled");
387
388 remove_dir_all(&self.output_path).await?;
389 Ok(())
390 }
391}
392
393pub(crate) fn inject_commands(
394 connection: &mut MinecraftConnection,
395 commands: Vec<Command>,
396) -> io::Result<()> {
397 trace!(
398 "Injecting commands:{}",
399 commands
400 .iter()
401 .map(|it| it.get_command())
402 .fold(String::new(), |joined, command| joined + "\n" + command)
403 );
404 connection.execute_commands(commands)?;
405 Ok(())
406}
407
408const MAIN_THREAD_ID: i32 = 0;
409
410#[derive(Clone, Copy, Debug, Eq, PartialEq)]
411enum ScopeKind {
412 SelectedEntityScores,
413}
414pub const SELECTED_ENTITY_SCORES: &str = "@s scores";
415impl ScopeKind {
416 fn get_display_name(&self) -> &'static str {
417 match self {
418 ScopeKind::SelectedEntityScores => SELECTED_ENTITY_SCORES,
419 }
420 }
421}
422
423struct ScopeReference {
424 frame_id: i32,
425 kind: ScopeKind,
426}
427
428pub struct McfunctionDebugAdapter {
429 message_sender: UnboundedSender<Either<ProtocolMessage, LogEvent>>,
430 client_session: Option<ClientSession>,
431}
432impl McfunctionDebugAdapter {
433 pub fn new(message_sender: UnboundedSender<Either<ProtocolMessage, LogEvent>>) -> Self {
434 McfunctionDebugAdapter {
435 message_sender,
436 client_session: None,
437 }
438 }
439
440 async fn on_stopped(
441 &mut self,
442 event: StoppedEvent,
443 context: &mut (impl DebugAdapterContext + Send),
444 ) -> io::Result<()> {
445 if let Some(client_session) = &mut self.client_session {
446 if let Some(minecraft_session) = &mut client_session.minecraft_session {
447 minecraft_session.stopped_data = Some(StoppedData {
448 position: event.position,
449 stack_trace: minecraft_session.get_stack_trace().await?,
450 });
451
452 let event = StoppedEventBody::builder()
453 .reason(to_stopped_event_reason(event.reason))
454 .thread_id(Some(MAIN_THREAD_ID))
455 .build();
456 context.fire_event(event);
457 }
458 }
459
460 Ok(())
461 }
462
463 async fn on_exited(
464 &mut self,
465 context: &mut (impl DebugAdapterContext + Send),
466 ) -> io::Result<()> {
467 if let Some(client_session) = &mut self.client_session {
468 if let Some(minecraft_session) = &mut client_session.minecraft_session {
469 minecraft_session.uninstall_datapack().await?;
470
471 context.fire_event(TerminatedEventBody::builder().build());
472 }
473 }
474
475 Ok(())
476 }
477
478 fn unwrap_client_session(
479 client_session: &mut Option<ClientSession>,
480 ) -> Result<&mut ClientSession, PartialErrorResponse> {
481 client_session.as_mut().ok_or_else(|| PartialErrorResponse {
482 message: "Not initialized".to_string(),
483 details: None,
484 })
485 }
486
487 fn unwrap_minecraft_session(
488 minecraft_session: &mut Option<MinecraftSession>,
489 ) -> Result<&mut MinecraftSession, PartialErrorResponse> {
490 minecraft_session
491 .as_mut()
492 .ok_or_else(|| PartialErrorResponse {
493 message: "Not launched or attached".to_string(),
494 details: None,
495 })
496 }
497
498 async fn continue_internal(
499 &mut self,
500 temporary_breakpoints: Vec<(ResourceLocation, LocalBreakpoint)>,
501 ) -> Result<(), RequestError<io::Error>> {
502 let client_session = Self::unwrap_client_session(&mut self.client_session)?;
503 let mc_session = Self::unwrap_minecraft_session(&mut client_session.minecraft_session)?;
504
505 if let Some(stopped_data) = mc_session.stopped_data.as_ref() {
506 let mut dirty = false;
507
508 if !client_session.temporary_breakpoints.is_empty() {
509 client_session.temporary_breakpoints.clear();
510 dirty = true;
511 }
512
513 for (function, breakpoint) in temporary_breakpoints {
514 client_session
515 .temporary_breakpoints
516 .insert(function, breakpoint);
517 dirty = true;
518 }
519
520 client_session.temporary_breakpoints.insert(
522 stopped_data.position.function.clone(),
523 LocalBreakpoint {
524 kind: BreakpointKind::Continue,
525 position: LocalBreakpointPosition {
526 line_number: stopped_data.position.line_number,
527 position_in_line: stopped_data.position.position_in_line,
528 },
529 },
530 );
531 if !can_resume_from(&client_session.breakpoints, &stopped_data.position) {
533 dirty = true;
534 }
535
536 let mut commands = Vec::new();
537
538 if dirty {
539 generate_datapack(
540 mc_session,
541 &client_session.breakpoints,
542 &client_session.temporary_breakpoints,
543 )
544 .await?;
545 commands.push(Command::new("reload"));
546 };
547
548 commands.push(Command::new("function debug:resume"));
549 mc_session.inject_commands(commands)?;
550 mc_session.stopped_data = None;
551 mc_session.scopes.clear();
552 }
553
554 Ok(())
555 }
556}
557
558#[async_trait]
559impl DebugAdapter for McfunctionDebugAdapter {
560 type Message = LogEvent;
561 type CustomError = io::Error;
562
563 async fn handle_other_message(
564 &mut self,
565 msg: Self::Message,
566 mut context: impl DebugAdapterContext + Send,
567 ) -> Result<(), Self::CustomError> {
568 trace!(
569 "Received message from Minecraft by {}: {}",
570 msg.executor,
571 msg.output
572 );
573 if let Ok(output) = msg.output.parse::<AddTagOutput>() {
574 if output.entity == LISTENER_NAME {
575 if let Ok(event) = output.tag.parse() {
576 self.on_stopped(event, &mut context).await?;
577 }
578 if output.tag == "exited" {
579 self.on_exited(&mut context).await?;
580 }
581 }
582 }
583 Ok(())
584 }
585
586 async fn continue_(
587 &mut self,
588 _args: ContinueRequestArguments,
589 _context: impl DebugAdapterContext + Send,
590 ) -> Result<ContinueResponseBody, RequestError<Self::CustomError>> {
591 self.continue_internal(Vec::new()).await?;
592
593 Ok(ContinueResponseBody::builder().build())
594 }
595
596 async fn evaluate(
597 &mut self,
598 _args: EvaluateRequestArguments,
599 _context: impl DebugAdapterContext + Send,
600 ) -> Result<EvaluateResponseBody, RequestError<Self::CustomError>> {
601 let client_session = Self::unwrap_client_session(&mut self.client_session)?;
602 let _mc_session = Self::unwrap_minecraft_session(&mut client_session.minecraft_session)?;
603
604 Err(RequestError::Respond(PartialErrorResponse::new(
605 "Not supported yet, see: \
606 https://github.com/vanilla-technologies/mcfunction-debugger/issues/68"
607 .to_string(),
608 )))
609 }
610
611 async fn initialize(
612 &mut self,
613 args: InitializeRequestArguments,
614 mut context: impl DebugAdapterContext + Send,
615 ) -> Result<Capabilities, RequestError<Self::CustomError>> {
616 let parser = CommandParser::default()
617 .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))
618 .map_err(Self::map_custom_error)?;
619 self.client_session = Some(ClientSession {
620 lines_start_at_1: args.lines_start_at_1,
621 columns_start_at_1: args.columns_start_at_1,
622 path_format: args.path_format,
623 minecraft_session: None,
624 breakpoints: MultiMap::new(),
625 temporary_breakpoints: MultiMap::new(),
626 parser,
627 });
628
629 context.fire_event(Event::Initialized);
630
631 Ok(Capabilities::builder()
632 .supports_cancel_request(true)
633 .supports_terminate_request(true)
634 .build())
635 }
636
637 async fn launch(
638 &mut self,
639 args: LaunchRequestArguments,
640 context: impl DebugAdapterContext + Send,
641 ) -> Result<(), RequestError<Self::CustomError>> {
642 let client_session = Self::unwrap_client_session(&mut self.client_session)?;
643
644 let config = get_config(&args)?;
645
646 let mut connection = establish_connection(
647 &config.minecraft_world_dir,
648 &config.minecraft_log_file,
649 context,
650 )
651 .await?;
652
653 let mut events = connection.add_named_listener(LISTENER_NAME);
654 let message_sender = self.message_sender.clone();
655 tokio::spawn(async move {
656 while let Some(event) = events.next().await {
657 if let Err(_) = message_sender.send(Either::Right(event)) {
658 break;
659 }
660 }
661 });
662
663 let namespace = "mcfd".to_string(); let debug_datapack_name = format!("debug-{}", config.datapack_name);
665 let output_path = config
666 .minecraft_world_dir
667 .join("datapacks")
668 .join(&debug_datapack_name);
669
670 let mut minecraft_session = MinecraftSession {
671 connection,
672 datapack: config.datapack.to_path_buf(),
673 namespace,
674 output_path,
675 scopes: Vec::new(),
676 stopped_data: None,
677 };
678
679 generate_datapack(
680 &minecraft_session,
681 &client_session.breakpoints,
682 &client_session.temporary_breakpoints,
683 )
684 .await?;
685
686 minecraft_session.inject_commands(vec![
687 Command::new("reload"),
688 Command::new(format!("datapack enable \"file/{}\"", debug_datapack_name)),
689 Command::new(format!(
692 "schedule function debug:{}/{} 1t",
693 config.function.namespace(),
694 config.function.path(),
695 )),
696 ])?;
697
698 client_session.minecraft_session = Some(minecraft_session);
699 Ok(())
700 }
701
702 async fn next(
703 &mut self,
704 _args: NextRequestArguments,
705 _context: impl DebugAdapterContext + Send,
706 ) -> Result<(), RequestError<Self::CustomError>> {
707 let client_session = Self::unwrap_client_session(&mut self.client_session)?;
708 let mc_session = Self::unwrap_minecraft_session(&mut client_session.minecraft_session)?;
709
710 let stack_trace = mc_session.get_cached_stack_trace()?;
711 let temporary_breakpoints = mc_session
712 .create_step_over_breakpoints(stack_trace, &client_session.parser)
713 .await?;
714 self.continue_internal(temporary_breakpoints).await?;
715
716 Ok(())
717 }
718
719 async fn pause(
720 &mut self,
721 _args: PauseRequestArguments,
722 mut context: impl DebugAdapterContext + Send,
723 ) -> Result<(), RequestError<Self::CustomError>> {
724 let client_session = Self::unwrap_client_session(&mut self.client_session)?;
725 let _mc_session = Self::unwrap_minecraft_session(&mut client_session.minecraft_session)?;
726
727 let event = OutputEventBody::builder()
728 .category(OutputCategory::Important)
729 .output("Minecraft cannot be paused".to_string())
730 .build();
731 context.fire_event(event);
732
733 Err(RequestError::Respond(PartialErrorResponse::new(
734 "Minecraft cannot be paused".to_string(),
735 )))
736 }
737
738 async fn scopes(
739 &mut self,
740 args: ScopesRequestArguments,
741 _context: impl DebugAdapterContext + Send,
742 ) -> Result<ScopesResponseBody, RequestError<Self::CustomError>> {
743 let client_session = Self::unwrap_client_session(&mut self.client_session)?;
744 let mc_session = Self::unwrap_minecraft_session(&mut client_session.minecraft_session)?;
745
746 let mut scopes = Vec::new();
747 let is_server_context = mc_session.get_context_entity_id(args.frame_id).await? == 0;
748 if !is_server_context {
749 scopes.push(create_selected_entity_scores_scope(mc_session, args));
750 }
751 Ok(ScopesResponseBody::builder().scopes(scopes).build().into())
752 }
753
754 async fn set_breakpoints(
755 &mut self,
756 args: SetBreakpointsRequestArguments,
757 _context: impl DebugAdapterContext + Send,
758 ) -> Result<SetBreakpointsResponseBody, RequestError<Self::CustomError>> {
759 let client_session = Self::unwrap_client_session(&mut self.client_session)?;
760
761 let offset = client_session.get_line_offset();
762 let path = match client_session.path_format {
763 PathFormat::Path => args.source.path.as_ref().ok_or_else(|| {
764 PartialErrorResponse::new("Missing argument source.path".to_string())
765 })?,
766 PathFormat::URI => todo!("Implement path URIs"),
767 };
768 let (_datapack, function) = parse_function_path(path.as_ref())
769 .map_err(|e| PartialErrorResponse::new(format!("Argument source.path {}", e)))?;
770
771 let breakpoints = args
772 .breakpoints
773 .iter()
774 .map(|source_breakpoint| (function.clone(), source_breakpoint.line as usize + offset))
775 .collect::<Vec<_>>();
776
777 let mut response = Vec::new();
778 let old_breakpoints = client_session
779 .breakpoints
780 .remove(&function)
781 .unwrap_or_default();
782 let mut new_breakpoints = Vec::with_capacity(breakpoints.len());
783 for (i, (function, line_number)) in breakpoints.into_iter().enumerate() {
784 let id = (i + client_session.breakpoints.len()) as i32;
785 let verified = verify_breakpoint(&client_session.parser, path, line_number)
786 .await
787 .map_err(|e| {
788 PartialErrorResponse::new(format!(
789 "Failed to verify breakpoint {}:{}: {}",
790 function, line_number, e
791 ))
792 })?;
793 new_breakpoints.push(LocalBreakpoint {
794 kind: if verified {
795 BreakpointKind::Normal
796 } else {
797 BreakpointKind::Invalid
798 },
799 position: LocalBreakpointPosition {
800 line_number,
801 position_in_line: BreakpointPositionInLine::Breakpoint,
802 },
803 });
804 response.push(
805 Breakpoint::builder()
806 .id(verified.then(|| id))
807 .verified(verified)
808 .line(Some((line_number - offset) as i32))
809 .build(),
810 );
811 }
812
813 client_session
814 .breakpoints
815 .insert_many(function.clone(), new_breakpoints);
816 let new_breakpoints = client_session.breakpoints.get_vec(&function).unwrap();
818
819 if let Some(minecraft_session) = client_session.minecraft_session.as_mut() {
820 generate_datapack(
821 minecraft_session,
822 &client_session.breakpoints,
823 &client_session.temporary_breakpoints,
824 )
825 .await?;
826 let mut commands = vec![Command::new("reload")];
827 if args.source_modified && old_breakpoints.len() == new_breakpoints.len() {
828 commands.extend(get_move_breakpoint_commands(
829 old_breakpoints.iter().map(|it| {
830 BreakpointPosition::from_breakpoint(function.clone(), &it.position)
831 }),
832 new_breakpoints.iter().map(|it| {
833 BreakpointPosition::from_breakpoint(function.clone(), &it.position)
834 }),
835 &minecraft_session.namespace,
836 ));
837 }
838 minecraft_session.inject_commands(commands)?;
839 }
840
841 Ok(SetBreakpointsResponseBody::builder()
842 .breakpoints(response)
843 .build())
844 }
845
846 async fn stack_trace(
847 &mut self,
848 _args: StackTraceRequestArguments,
849 _context: impl DebugAdapterContext + Send,
850 ) -> Result<StackTraceResponseBody, RequestError<Self::CustomError>> {
851 let client_session = Self::unwrap_client_session(&mut self.client_session)?;
852 let get_line_offset = client_session.get_line_offset();
853 let get_column_offset = client_session.get_column_offset();
854 let mc_session = Self::unwrap_minecraft_session(&mut client_session.minecraft_session)?;
855
856 let stack_trace = mc_session
857 .get_cached_stack_trace()?
858 .into_iter()
859 .map(|it| it.to_stack_frame(&mc_session.datapack, get_line_offset, get_column_offset))
860 .collect::<Vec<_>>();
861
862 Ok(StackTraceResponseBody::builder()
863 .total_frames(Some(stack_trace.len() as i32))
864 .stack_frames(stack_trace)
865 .build())
866 }
867
868 async fn terminate(
869 &mut self,
870 _args: TerminateRequestArguments,
871 _context: impl DebugAdapterContext + Send,
872 ) -> Result<(), RequestError<Self::CustomError>> {
873 if let Some(client_session) = &mut self.client_session {
874 if let Some(minecraft_session) = &mut client_session.minecraft_session {
875 minecraft_session.inject_commands(vec![Command::new("function debug:stop")])?;
876 }
877 }
878 Ok(())
879 }
880
881 async fn step_in(
882 &mut self,
883 _args: StepInRequestArguments,
884 _context: impl DebugAdapterContext + Send,
885 ) -> Result<(), RequestError<Self::CustomError>> {
886 let client_session = Self::unwrap_client_session(&mut self.client_session)?;
887 let mc_session = Self::unwrap_minecraft_session(&mut client_session.minecraft_session)?;
888
889 let stack_trace = mc_session.get_cached_stack_trace()?;
890 let temporary_breakpoints = mc_session
891 .create_step_in_breakpoints(stack_trace, &client_session.parser)
892 .await?;
893 self.continue_internal(temporary_breakpoints).await?;
894
895 Ok(())
896 }
897
898 async fn step_out(
899 &mut self,
900 _args: StepOutRequestArguments,
901 _context: impl DebugAdapterContext + Send,
902 ) -> Result<(), RequestError<Self::CustomError>> {
903 let client_session = Self::unwrap_client_session(&mut self.client_session)?;
904 let mc_session = Self::unwrap_minecraft_session(&mut client_session.minecraft_session)?;
905
906 let stack_trace = mc_session.get_cached_stack_trace()?;
907 let temporary_breakpoints = mc_session
908 .create_step_out_breakpoint(&stack_trace, &client_session.parser)
909 .await?;
910 self.continue_internal(temporary_breakpoints).await?;
911
912 Ok(())
913 }
914
915 async fn threads(
916 &mut self,
917 _context: impl DebugAdapterContext + Send,
918 ) -> Result<ThreadsResponseBody, RequestError<Self::CustomError>> {
919 let client_session = Self::unwrap_client_session(&mut self.client_session)?;
920 let _mc_session = Self::unwrap_minecraft_session(&mut client_session.minecraft_session)?;
921
922 let thread = Thread::builder()
923 .id(MAIN_THREAD_ID)
924 .name("Main Thread".to_string())
925 .build();
926 Ok(ThreadsResponseBody::builder()
927 .threads(vec![thread])
928 .build()
929 .into())
930 }
931
932 async fn variables(
933 &mut self,
934 args: VariablesRequestArguments,
935 _context: impl DebugAdapterContext + Send,
936 ) -> Result<VariablesResponseBody, RequestError<Self::CustomError>> {
937 let client_session = Self::unwrap_client_session(&mut self.client_session)?;
938 let mc_session = Self::unwrap_minecraft_session(&mut client_session.minecraft_session)?;
939
940 let unknown_variables_reference = || {
941 PartialErrorResponse::new(format!(
942 "Unknown variables_reference: {}",
943 args.variables_reference
944 ))
945 };
946 let scope_id = usize::try_from(args.variables_reference - 1)
947 .map_err(|_| unknown_variables_reference())?;
948 let scope: &ScopeReference = mc_session
949 .scopes
950 .get(scope_id)
951 .ok_or_else(unknown_variables_reference)?;
952
953 const START: &str = "variables.start";
954 const END: &str = "variables.end";
955
956 match scope.kind {
957 ScopeKind::SelectedEntityScores => {
958 let events = mc_session.connection.add_listener();
959
960 let execute_as_context = format!(
961 "execute as @e[\
962 type=area_effect_cloud,\
963 tag=-ns-_context,\
964 tag=-ns-_active,\
965 tag=-ns-_current,\
966 scores={{-ns-_depth={}}},\
967 ] run",
968 scope.frame_id
969 );
970 let decrement_ids = mc_session.replace_ns(&format!(
971 "{} scoreboard players operation @e[tag=!-ns-_context] -ns-_id -= @s -ns-_id",
972 execute_as_context
973 ));
974 let increment_ids = mc_session.replace_ns(&format!(
975 "{} scoreboard players operation @e[tag=!-ns-_context] -ns-_id += @s -ns-_id",
976 execute_as_context
977 ));
978 mc_session.inject_commands(vec![
979 Command::new(logged_command(enable_logging_command())),
980 Command::new(named_logged_command(
981 LISTENER_NAME,
982 summon_named_entity_command(START),
983 )),
984 Command::new(logged_command(decrement_ids)),
985 Command::new(mc_session.replace_ns("function -ns-:log_scores")),
986 Command::new(logged_command(increment_ids)),
987 Command::new(named_logged_command(
988 LISTENER_NAME,
989 summon_named_entity_command(END),
990 )),
991 Command::new(logged_command(reset_logging_command())),
992 ])?;
993
994 let variables = events_between(events, START, END)
995 .filter_map(|event| event.output.parse::<QueryScoreboardOutput>().ok())
996 .map(|output| {
997 Variable::builder()
998 .name(output.scoreboard)
999 .value(output.score.to_string())
1000 .variables_reference(0)
1001 .build()
1002 })
1003 .collect::<Vec<_>>()
1004 .await;
1005
1006 Ok(VariablesResponseBody::builder()
1007 .variables(variables)
1008 .build())
1009 }
1010 }
1011 }
1012}
1013
1014async fn find_first_target_line_number(
1015 path: impl AsRef<Path>,
1016 parser: &CommandParser,
1017) -> Result<usize, RequestError<io::Error>> {
1018 Ok(find_step_target_line_number(&path, 0, parser, false)
1019 .await?
1020 .unwrap_or(1))
1021}
1022
1023async fn find_step_target_line_number(
1025 path: impl AsRef<Path>,
1026 after_line_number: usize,
1027 parser: &CommandParser,
1028 allow_empty_lines: bool,
1029) -> Result<Option<usize>, RequestError<io::Error>> {
1030 let content = read_to_string(&path).await.map_err(|e| {
1031 PartialErrorResponse::new(format!(
1032 "Failed to read file {}: {}",
1033 path.as_ref().display(),
1034 e
1035 ))
1036 })?;
1037
1038 let lines = content.split('\n').enumerate().skip(after_line_number);
1039 let mut last_line_of_file = true;
1040 for (line_index, line) in lines {
1041 last_line_of_file = false;
1042 let line = line.strip_suffix('\r').unwrap_or(line); let line = parse_line(parser, &line, false);
1044 if is_command(line) {
1045 let line_number = line_index + 1;
1046 return Ok(Some(line_number));
1047 }
1048 }
1049
1050 if last_line_of_file {
1051 Ok(None)
1052 } else {
1053 if allow_empty_lines {
1054 Ok(Some(after_line_number + 1)) } else {
1056 Ok(None)
1057 }
1058 }
1059}
1060
1061async fn get_function_command(
1062 path: impl AsRef<Path>,
1063 line_number: usize,
1064 parser: &CommandParser,
1065) -> Result<Option<ResourceLocation>, RequestError<io::Error>> {
1066 let file = File::open(&path).await.map_err(|e| {
1067 PartialErrorResponse::new(format!(
1068 "Failed to open file {}: {}",
1069 path.as_ref().display(),
1070 e
1071 ))
1072 })?;
1073 let lines = BufReader::new(file).lines();
1074 let mut lines = LinesStream::new(lines).skip(line_number - 1);
1075 if let Some(line) = lines.next().await {
1076 let line = line.map_err(|e| {
1077 PartialErrorResponse::new(format!(
1078 "Failed to read file {}: {}",
1079 path.as_ref().display(),
1080 e
1081 ))
1082 })?;
1083
1084 let line = parse_line(parser, &line, false);
1085 if let Line::FunctionCall { name, .. } = line {
1086 return Ok(Some(name));
1087 }
1088 }
1089 Ok(None)
1090}
1091
1092struct Config<'l> {
1093 datapack: &'l Path,
1094 datapack_name: &'l str,
1095 function: ResourceLocation,
1096 minecraft_world_dir: &'l Path,
1097 minecraft_log_file: &'l Path,
1098}
1099
1100fn get_config(args: &LaunchRequestArguments) -> Result<Config, PartialErrorResponse> {
1101 let program = get_path(&args, "program")?;
1102
1103 let (datapack, function) = parse_function_path(program)
1104 .map_err(|e| PartialErrorResponse::new(format!("Attribute 'program' {}", e)))?;
1105
1106 let datapack_name = datapack
1107 .file_name()
1108 .ok_or_else(|| {
1109 PartialErrorResponse::new(format!(
1110 "Attribute 'program' contains an invalid path: {}",
1111 program.display()
1112 ))
1113 })?
1114 .to_str()
1115 .unwrap(); let minecraft_world_dir = get_path(&args, "minecraftWorldDir")?;
1118 let minecraft_log_file = get_path(&args, "minecraftLogFile")?;
1119 Ok(Config {
1120 datapack,
1121 datapack_name,
1122 function,
1123 minecraft_world_dir,
1124 minecraft_log_file,
1125 })
1126}
1127
1128fn get_path<'a>(
1129 args: &'a LaunchRequestArguments,
1130 key: &str,
1131) -> Result<&'a Path, PartialErrorResponse> {
1132 let value = args
1133 .additional_attributes
1134 .get(key)
1135 .ok_or_else(|| PartialErrorResponse::new(format!("Missing attribute '{}'", key)))?
1136 .as_str()
1137 .ok_or_else(|| {
1138 PartialErrorResponse::new(format!("Attribute '{}' is not of type string", key))
1139 })?;
1140 let value = Path::new(value);
1141 Ok(value)
1142}
1143
1144fn create_selected_entity_scores_scope(
1145 mc_session: &mut MinecraftSession,
1146 args: ScopesRequestArguments,
1147) -> Scope {
1148 let kind = ScopeKind::SelectedEntityScores;
1149 mc_session.scopes.push(ScopeReference {
1150 frame_id: args.frame_id,
1151 kind,
1152 });
1153 let variables_reference = mc_session.scopes.len();
1154 Scope::builder()
1155 .name(kind.get_display_name().to_string())
1156 .variables_reference(variables_reference as i32)
1157 .expensive(false)
1158 .build()
1159}
1160
1161async fn verify_breakpoint(
1162 parser: &CommandParser,
1163 path: impl AsRef<Path>,
1164 line_number: usize,
1165) -> io::Result<bool> {
1166 let file = File::open(path).await?;
1167 let lines = BufReader::new(file).lines();
1168 if let Some(result) = LinesStream::new(lines).skip(line_number - 1).next().await {
1169 let line = result?;
1170 let line = parse_line(parser, &line, false);
1171 return Ok(is_command(line));
1172 } else {
1173 Ok(false)
1174 }
1175}
1176fn get_move_breakpoint_commands(
1177 old_positions: impl ExactSizeIterator<Item = BreakpointPosition>,
1178 new_positions: impl ExactSizeIterator<Item = BreakpointPosition>,
1179 namespace: &str,
1180) -> Vec<Command> {
1181 let tmp_tag = format!("{}_tmp", namespace);
1182 let breakpoint_tag = format!("{}_breakpoint", namespace);
1183 let mut commands = Vec::new();
1184 for (old_position, new_position) in old_positions.zip(new_positions) {
1185 if old_position != new_position {
1186 let old_tag = format!("{}+{}", namespace, old_position);
1187 let new_tag = format!("{}+{}", namespace, new_position);
1188 commands.push(Command::new(format!(
1189 "tag @e[tag={},tag={},tag=!{}] add {}",
1190 breakpoint_tag, old_tag, tmp_tag, new_tag,
1191 )));
1192 commands.push(Command::new(format!(
1193 "tag @e[tag={},tag={}] add {}",
1194 breakpoint_tag, old_tag, tmp_tag
1195 )));
1196 commands.push(Command::new(format!(
1197 "tag @e[tag={},tag={},tag={}] remove {}",
1198 breakpoint_tag, old_tag, new_tag, old_tag
1199 )));
1200 }
1201 }
1202 commands.push(Command::new(format!(
1203 "tag @e[tag={},tag={}] remove {}",
1204 breakpoint_tag, tmp_tag, tmp_tag
1205 )));
1206 commands
1207}
1208
1209fn is_command(line: Line) -> bool {
1210 !matches!(line, Line::Empty | Line::Comment | Line::Breakpoint)
1211}