mcfunction_debugger/
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-2024 Adrodoc <adrodoc55@googlemail.com> & Skagaros <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        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        // Continue must be scheduled to ensure it runs before suspended schedules
525        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(); // Hardcoded in installer as well
656        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        // After loading the datapack we must wait one tick for it to install itself
692        let mut commands = mc_session.setup_breakpoint_commands(&client_session.breakpoints);
693        // By scheduling this function call we have a defined execution position
694        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        // Unwrap is safe, because we just inserted the value
801        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            // TODO: https://codeberg.org/vanilla-technologies/mcfunction-debugger/issues/70
812            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(); // Path is known to be UTF-8
1006
1007    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}