mcfunction_debug_adapter/
adapter.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
19pub 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); // should not happen
148        }
149        let current = &stack_trace[0];
150        let current_depth = stack_trace.len() - 1;
151        let current_path = self.get_function_path(&current.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); // should not happen
184        }
185        let current = &stack_trace[0];
186        let current_depth = stack_trace.len() - 1;
187        let current_path = self.get_function_path(&current.location.function);
188
189        let next_line_number = find_step_target_line_number(
190            &current_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            // Reentry for next executor
210            let first_line_number = find_first_target_line_number(&current_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 // depth
351                } 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 // Breakpoint
358                } else {
359                    continue; // Shouldn't actually happen
360                };
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            // Always insert continue point to avoid a race condition where the user removes the breakpoint right before Minecraft continues
521            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 there isn't already a breakpoint that can resume we need to load the continue point
532            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(); // Hardcoded in installer as well
664        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            // After loading the datapack we must wait one tick for it to install itself
690            // By scheduling this function call we also have a defined execution position
691            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        // Unwrap is safe, because we just inserted the value
817        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
1023// TODO: replace allow_empty_lines with custom enum return type
1024async 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); // Remove trailing carriage return on Windows
1043        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)) // This line is empty or a comment
1055        } 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(); // Path is known to be UTF-8
1116
1117    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}