use crate::{
adapter::{MinecraftSession, LISTENER_NAME},
error::PartialErrorResponse,
};
use debug_adapter_protocol::{
events::StoppedEventReason,
types::{Source, StackFrame},
};
use futures::Stream;
use mcfunction_debugger::{
config::{
adapter::{
AdapterConfig, BreakpointKind, BreakpointPositionInLine, LocalBreakpoint,
LocalBreakpointPosition,
},
Config,
},
generate_debug_datapack,
parser::command::resource_location::ResourceLocation,
StoppedReason,
};
use minect::{command::SummonNamedEntityOutput, log::LogEvent};
use multimap::MultiMap;
use std::{fmt::Display, path::Path, str::FromStr};
use tokio::fs::remove_dir_all;
use tokio_stream::StreamExt;
pub fn parse_function_path(path: &Path) -> Result<(&Path, ResourceLocation), String> {
let datapack = find_parent_datapack(path).ok_or_else(|| {
format!(
"does not denote a path in a datapack directory with a pack.mcmeta file: {}",
&path.display()
)
})?;
let data_path = path.strip_prefix(datapack.join("data")).map_err(|_| {
format!(
"does not denote a path in the data directory of datapack {}: {}",
&datapack.display(),
&path.display()
)
})?;
let function = get_function_name(data_path, &path)?;
Ok((datapack, function))
}
pub fn find_parent_datapack(mut path: &Path) -> Option<&Path> {
while let Some(p) = path.parent() {
path = p;
let pack_mcmeta_path = path.join("pack.mcmeta");
if pack_mcmeta_path.is_file() {
return Some(path);
}
}
None
}
pub fn get_function_name(
data_path: impl AsRef<Path>,
path: impl AsRef<Path>,
) -> Result<ResourceLocation, String> {
let namespace = data_path.as_ref()
.iter()
.next()
.ok_or_else(|| {
format!(
"contains an invalid path: {}",
path.as_ref().display()
)
})?
.to_str()
.unwrap() ;
let fn_path = data_path
.as_ref()
.strip_prefix(Path::new(namespace).join("functions"))
.map_err(|_| format!("contains an invalid path: {}", path.as_ref().display()))?
.with_extension("")
.to_str()
.unwrap() .replace(std::path::MAIN_SEPARATOR, "/");
Ok(ResourceLocation::new(&namespace, &fn_path))
}
pub(super) async fn generate_datapack(
minecraft_session: &MinecraftSession,
breakpoints: &MultiMap<ResourceLocation, LocalBreakpoint>,
temporary_breakpoints: &MultiMap<ResourceLocation, LocalBreakpoint>,
) -> Result<(), PartialErrorResponse> {
let mut breakpoints = breakpoints.clone();
for (key, values) in temporary_breakpoints.iter_all() {
for value in values {
if !contains_breakpoint(
&breakpoints,
&BreakpointPosition::from_breakpoint(key.clone(), &value.position),
) {
breakpoints.insert(key.clone(), value.clone());
}
}
}
let config = Config {
namespace: &minecraft_session.namespace,
shadow: false,
adapter: Some(AdapterConfig {
adapter_listener_name: LISTENER_NAME,
breakpoints: &breakpoints,
}),
};
let _ = remove_dir_all(&minecraft_session.output_path).await;
generate_debug_datapack(
&minecraft_session.datapack,
&minecraft_session.output_path,
&config,
)
.await
.map_err(|e| PartialErrorResponse::new(format!("Failed to generate debug datapack: {}", e)))?;
Ok(())
}
pub(crate) fn can_resume_from(
breakpoints: &MultiMap<ResourceLocation, LocalBreakpoint>,
position: &BreakpointPosition,
) -> bool {
get_breakpoint_kind(breakpoints, position)
.map(|it| it.can_resume())
.unwrap_or(false)
}
pub(crate) fn contains_breakpoint(
breakpoints: &MultiMap<ResourceLocation, LocalBreakpoint>,
position: &BreakpointPosition,
) -> bool {
get_breakpoint_kind(breakpoints, position).is_some()
}
pub(crate) fn get_breakpoint_kind<'l>(
breakpoints: &'l MultiMap<ResourceLocation, LocalBreakpoint>,
position: &BreakpointPosition,
) -> Option<&'l BreakpointKind> {
if let Some(breakpoints) = breakpoints.get_vec(&position.function) {
breakpoints
.iter()
.filter(|it| it.position.line_number == position.line_number)
.filter(|it| it.position.position_in_line == position.position_in_line)
.map(|it| &it.kind)
.next()
} else {
None
}
}
pub(crate) fn events_between<'l>(
events: impl Stream<Item = LogEvent> + 'l,
start: &'l str,
stop: &'l str,
) -> impl Stream<Item = LogEvent> + 'l {
events
.skip_while(move |event| !is_summon_output(event, start))
.skip(1) .take_while(move |event| !is_summon_output(event, stop))
}
fn is_summon_output(event: &LogEvent, name: &str) -> bool {
event.executor == LISTENER_NAME
&& event
.output
.parse::<SummonNamedEntityOutput>()
.ok()
.filter(|output| output.name == name)
.is_some()
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) struct BreakpointPosition {
pub(crate) function: ResourceLocation,
pub(crate) line_number: usize,
pub(crate) position_in_line: BreakpointPositionInLine,
}
impl BreakpointPosition {
pub(crate) fn from_breakpoint(
function: ResourceLocation,
position: &LocalBreakpointPosition,
) -> BreakpointPosition {
BreakpointPosition {
function,
line_number: position.line_number,
position_in_line: position.position_in_line,
}
}
}
impl FromStr for BreakpointPosition {
type Err = ();
fn from_str(string: &str) -> Result<Self, Self::Err> {
fn from_str_inner(string: &str) -> Option<BreakpointPosition> {
let (function, position) = string.rsplit_once('+')?;
let (line_number, position_in_line) = position.split_once('_')?;
let function = parse_resource_location(function, '+')?;
let line_number = line_number.parse().ok()?;
let position_in_line = position_in_line.parse().ok()?;
Some(BreakpointPosition {
function,
line_number,
position_in_line,
})
}
from_str_inner(string).ok_or(())
}
}
impl Display for BreakpointPosition {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}+{}+{}_{}",
self.function.namespace(),
self.function.path().replace("/", "+"),
self.line_number,
self.position_in_line,
)
}
}
pub(crate) struct StoppedData {
pub(crate) position: BreakpointPosition,
pub(crate) stack_trace: Vec<McfunctionStackFrame>,
}
pub(crate) struct StoppedEvent {
pub(crate) reason: StoppedReason,
pub(crate) position: BreakpointPosition,
}
impl FromStr for StoppedEvent {
type Err = ();
fn from_str(string: &str) -> Result<Self, Self::Err> {
fn from_str_inner(string: &str) -> Option<StoppedEvent> {
let string = string.strip_prefix("stopped+")?;
let (reason, position) = string.split_once('+')?;
let reason = reason.parse().ok()?;
let position = position.parse().ok()?;
Some(StoppedEvent { reason, position })
}
from_str_inner(string).ok_or(())
}
}
pub(crate) fn to_stopped_event_reason(reason: StoppedReason) -> StoppedEventReason {
match reason {
StoppedReason::Breakpoint => StoppedEventReason::Breakpoint,
StoppedReason::Step => StoppedEventReason::Step,
}
}
pub(crate) struct McfunctionStackFrame {
pub(crate) id: i32,
pub(crate) location: SourceLocation,
}
impl McfunctionStackFrame {
pub(crate) fn to_stack_frame(
&self,
datapack: impl AsRef<Path>,
line_offset: usize,
column_offset: usize,
) -> StackFrame {
let path = datapack
.as_ref()
.join("data")
.join(self.location.function.mcfunction_path())
.display()
.to_string();
StackFrame::builder()
.id(self.id)
.name(self.location.get_name())
.source(Some(Source::builder().path(Some(path)).build()))
.line((self.location.line_number - line_offset) as i32)
.column((self.location.column_number - column_offset) as i32)
.build()
}
}
#[derive(Clone, Debug)]
pub(crate) struct SourceLocation {
pub(crate) function: ResourceLocation,
pub(crate) line_number: usize,
pub(crate) column_number: usize,
}
impl FromStr for SourceLocation {
type Err = ();
fn from_str(string: &str) -> Result<Self, Self::Err> {
fn from_str_inner(string: &str) -> Option<SourceLocation> {
let has_column = 3 == string.bytes().filter(|b| *b == b':').count();
let (function_line_number, column_number) = if has_column {
let (function_line_number, column_number) = string.rsplit_once(':')?;
let column_number = column_number.parse().ok()?;
(function_line_number, column_number)
} else {
(string, 1)
};
let (function, line_number) = function_line_number.rsplit_once(':')?;
let function = parse_resource_location(function, ':')?;
let line_number = line_number.parse().ok()?;
Some(SourceLocation {
function,
line_number,
column_number,
})
}
from_str_inner(string).ok_or(())
}
}
impl SourceLocation {
pub(crate) fn get_name(&self) -> String {
format!("{}:{}", self.function, self.line_number)
}
}
fn parse_resource_location(function: &str, seperator: char) -> Option<ResourceLocation> {
if let [orig_ns, orig_fn @ ..] = function.split(seperator).collect::<Vec<_>>().as_slice() {
Some(ResourceLocation::new(orig_ns, &orig_fn.join("/")))
} else {
None
}
}