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