mcfunction_debugger/generator/
parser.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 command;
20
21use crate::generator::parser::command::{
22    argument::{
23        minecraft::{MinecraftEntityAnchor, MinecraftMessage, MinecraftScoreHolder, MinecraftTime},
24        Argument,
25    },
26    resource_location::{ResourceLocation, ResourceLocationRef},
27    CommandParser, CommandParserError, CommandParserResult, ParsedNode,
28};
29use log::debug;
30use std::{
31    collections::{BTreeMap, BTreeSet},
32    convert::TryFrom,
33    usize,
34};
35
36use self::command::argument::minecraft::{
37    entity::{MinecraftEntity, MinecraftSelector, MinecraftSelectorType},
38    MinecraftFunction, MinecraftObjective,
39};
40
41#[derive(Clone, Debug, PartialEq)]
42pub enum Line {
43    Empty,
44    Comment,
45    FunctionCall {
46        column_index: usize,
47        name: ResourceLocation,
48        anchor: Option<MinecraftEntityAnchor>,
49        selectors: BTreeMap<usize, SelectorValue>,
50        objectives: BTreeSet<String>,
51    },
52    OptionalSelectorCommand {
53        missing_selector: usize,
54        selectors: BTreeMap<usize, SelectorValue>,
55        objectives: BTreeSet<String>,
56    },
57    Schedule {
58        schedule_start: usize,
59        function: ResourceLocation,
60        operation: ScheduleOperation,
61        selectors: BTreeMap<usize, SelectorValue>,
62        objectives: BTreeSet<String>,
63    },
64    OtherCommand {
65        selectors: BTreeMap<usize, SelectorValue>,
66        objectives: BTreeSet<String>,
67    },
68}
69
70impl Line {
71    pub fn objectives(&self) -> Option<&BTreeSet<String>> {
72        match self {
73            Line::FunctionCall { objectives, .. }
74            | Line::OptionalSelectorCommand { objectives, .. }
75            | Line::Schedule { objectives, .. }
76            | Line::OtherCommand { objectives, .. } => Some(objectives),
77            _ => None,
78        }
79    }
80}
81
82#[derive(Clone, Debug, PartialEq)]
83pub enum ScheduleOperation {
84    Append { time: MinecraftTime },
85    Clear,
86    Replace { time: MinecraftTime },
87}
88
89pub fn parse_line(parser: &CommandParser, line: &str) -> Line {
90    let (line, error) = parse_line_internal(parser, line);
91    if let Some(error) = error {
92        debug!("Failed to parse command: {}", error);
93    }
94    line
95}
96
97fn parse_line_internal<'l>(
98    parser: &'l CommandParser,
99    line: &'l str,
100) -> (Line, Option<CommandParserError<'l>>) {
101    let line = line.trim();
102    if line.starts_with('#') {
103        (Line::Comment, None)
104    } else if line.is_empty() {
105        (Line::Empty, None)
106    } else {
107        parse_command(parser, line)
108    }
109}
110
111fn parse_command<'l>(
112    parser: &'l CommandParser,
113    command: &'l str,
114) -> (Line, Option<CommandParserError<'l>>) {
115    let CommandParserResult {
116        parsed_nodes,
117        error,
118    } = parser.parse(command);
119    let mut nodes = parsed_nodes.as_slice();
120    let mut selectors = BTreeMap::new();
121    let mut objectives = BTreeSet::new();
122    let mut maybe_anchor: Option<MinecraftEntityAnchor> = None;
123
124    while let [_, tail @ ..] = nodes {
125        match nodes {
126            [ParsedNode::Argument {
127                argument: Argument::MinecraftEntity(entity),
128                index,
129                ..
130            }, ..] => {
131                selectors.insert(*index, SelectorValue::from(entity));
132            }
133            [ParsedNode::Argument {
134                argument: Argument::MinecraftScoreHolder(MinecraftScoreHolder::Selector(selector)),
135                index,
136                ..
137            }, ..] => {
138                selectors.insert(*index, SelectorValue::from(selector));
139            }
140
141            [ParsedNode::Argument {
142                argument:
143                    Argument::MinecraftMessage(MinecraftMessage {
144                        selectors: message_selectors,
145                        ..
146                    }),
147                index,
148                ..
149            }, ..] => {
150                selectors.extend(
151                    message_selectors.iter().map(|(selector, start, _end)| {
152                        (index + start, SelectorValue::from(selector))
153                    }),
154                );
155            }
156
157            [ParsedNode::Argument {
158                argument: Argument::MinecraftObjective(MinecraftObjective(objective)),
159                ..
160            }, ..]
161            | [ParsedNode::Literal {
162                literal: "scoreboard",
163                ..
164            }, ParsedNode::Literal {
165                literal: "objectives",
166                ..
167            }, ParsedNode::Literal { literal: "add", .. }, ParsedNode::Argument {
168                argument: Argument::BrigadierString(objective),
169                ..
170            }, ..] => {
171                objectives.insert(objective.to_string());
172            }
173
174            [ParsedNode::Literal {
175                literal: "execute", ..
176            }
177            | ParsedNode::Redirect("execute"), ParsedNode::Literal {
178                literal: "anchored",
179                ..
180            }, ParsedNode::Argument {
181                argument: Argument::MinecraftEntityAnchor(anchor),
182                ..
183            }, ..] => {
184                maybe_anchor = Some(*anchor);
185            }
186
187            _ => {}
188        }
189
190        nodes = tail;
191    }
192
193    if error.is_none() {
194        if let Some((column_index, name)) = as_function_call(&parsed_nodes) {
195            return (
196                Line::FunctionCall {
197                    column_index,
198                    name,
199                    anchor: maybe_anchor,
200                    selectors,
201                    objectives,
202                },
203                None,
204            );
205        }
206
207        if let Some((schedule_start, function, operation)) = as_schedule(&parsed_nodes) {
208            return (
209                Line::Schedule {
210                    schedule_start,
211                    function: function.to_owned(),
212                    operation,
213                    selectors,
214                    objectives,
215                },
216                None,
217            );
218        }
219
220        if let Some(missing_selector) = find_missing_selector(&parsed_nodes) {
221            return (
222                Line::OptionalSelectorCommand {
223                    missing_selector,
224                    selectors,
225                    objectives,
226                },
227                None,
228            );
229        }
230    }
231
232    (
233        Line::OtherCommand {
234            selectors,
235            objectives,
236        },
237        error,
238    )
239}
240
241fn as_function_call(nodes: &[ParsedNode]) -> Option<(usize, ResourceLocation)> {
242    if let [.., ParsedNode::Literal {
243        literal: "function",
244        index,
245        ..
246    }, ParsedNode::Argument {
247        argument: Argument::MinecraftFunction(MinecraftFunction(function)),
248        ..
249    }] = nodes
250    {
251        Some((*index, function.to_owned()))
252    } else {
253        None
254    }
255}
256
257fn as_schedule(mut nodes: &[ParsedNode]) -> Option<(usize, ResourceLocation, ScheduleOperation)> {
258    while let [_, tail @ ..] = nodes {
259        match nodes {
260            [ParsedNode::Literal {
261                literal: "schedule",
262                index,
263                ..
264            }, ParsedNode::Literal {
265                literal: "function",
266                ..
267            }, ParsedNode::Argument {
268                argument: Argument::MinecraftFunction(MinecraftFunction(function)),
269                ..
270            }, ParsedNode::Argument {
271                argument: Argument::MinecraftTime(time),
272                ..
273            }, tail @ ..] => {
274                let op = match tail {
275                    [ParsedNode::Literal {
276                        literal: "append", ..
277                    }] => Some(ScheduleOperation::Append { time: time.clone() }),
278                    []
279                    | [ParsedNode::Literal {
280                        literal: "replace", ..
281                    }] => Some(ScheduleOperation::Replace { time: time.clone() }),
282                    _ => None,
283                };
284                if let Some(op) = op {
285                    return Some((*index, function.to_owned(), op));
286                }
287            }
288
289            [ParsedNode::Literal {
290                literal: "schedule",
291                index,
292                ..
293            }, ParsedNode::Literal {
294                literal: "clear", ..
295            }, ParsedNode::Argument {
296                argument: Argument::BrigadierString(string),
297                ..
298            }] => {
299                // TODO Handle invalid characters in NamespacedName
300                if let Ok(function) = ResourceLocationRef::try_from(*string) {
301                    return Some((*index, function.to_owned(), ScheduleOperation::Clear));
302                }
303            }
304            _ => {}
305        }
306
307        nodes = tail;
308    }
309
310    None
311}
312
313fn find_missing_selector(tail: &[ParsedNode]) -> Option<usize> {
314    match tail {
315        [.., ParsedNode::Literal {
316            literal: kill @ "kill",
317            index,
318        }] => Some(index + kill.len()),
319
320        [.., ParsedNode::Literal {
321            literal: "team", ..
322        }, ParsedNode::Literal {
323            literal: "join", ..
324        }, ParsedNode::Argument {
325            argument: Argument::MinecraftTeam(..),
326            index,
327            len,
328            ..
329        }] => Some(index + len),
330
331        [.., ParsedNode::Redirect("teleport")
332        | ParsedNode::Literal {
333            literal: "teleport",
334            ..
335        }, ParsedNode::Argument {
336            name: "destination",
337            argument: Argument::MinecraftEntity(..),
338            index,
339            ..
340        }
341        | ParsedNode::Argument {
342            name: "location",
343            argument: Argument::MinecraftVec3(..),
344            index,
345            ..
346        }] => Some(index - 1),
347
348        _ => None,
349    }
350}
351
352#[derive(Clone, Debug, PartialEq)]
353pub struct SelectorValue {
354    pub exclude_minect_cursor: bool,
355}
356impl SelectorValue {
357    fn entity_exclude_minect_cursor(value: &MinecraftEntity<'_>) -> bool {
358        match value {
359            MinecraftEntity::Selector(selector) => Self::selector_exclude_minect_cursor(selector),
360            MinecraftEntity::PlayerNameOrUuid(..) => false,
361        }
362    }
363
364    fn selector_exclude_minect_cursor(value: &MinecraftSelector<'_>) -> bool {
365        value.selector_type != MinecraftSelectorType::S
366            && !value.tags.iter().any(|it| it.string == "minect_cursor")
367    }
368
369    pub(crate) fn empty_self() -> SelectorValue {
370        Self {
371            exclude_minect_cursor: false,
372        }
373    }
374}
375impl Default for SelectorValue {
376    fn default() -> Self {
377        Self {
378            exclude_minect_cursor: true,
379        }
380    }
381}
382impl From<&MinecraftSelector<'_>> for SelectorValue {
383    fn from(value: &MinecraftSelector<'_>) -> Self {
384        Self {
385            exclude_minect_cursor: Self::selector_exclude_minect_cursor(value),
386        }
387    }
388}
389impl From<&MinecraftEntity<'_>> for SelectorValue {
390    fn from(value: &MinecraftEntity<'_>) -> Self {
391        Self {
392            exclude_minect_cursor: Self::entity_exclude_minect_cursor(value),
393        }
394    }
395}
396
397#[cfg(test)]
398mod tests;