///|
/// Parser options for the line-based MoonBTKit DSL.
pub(all) struct DslOptions {
allow_comments : Bool
validate_after_parse : Bool
default_root : String
} derive(Debug, Eq)
///|
pub fn default_dsl_options() -> DslOptions {
{ allow_comments: true, validate_after_parse: true, default_root: "root" }
}
///|
pub(all) struct DslDocument {
tree : BehaviorTree
blackboard : Blackboard
warnings : Array[String]
} derive(Debug, Eq)
///|
pub fn DslDocument::summary(self : DslDocument) -> String {
"root=" +
self.tree.root +
", nodes=" +
self.tree.node_count().to_string() +
", blackboard=" +
self.blackboard.len().to_string() +
", warnings=" +
self.warnings.length().to_string()
}
///|
/// Parse a behavior tree and initial blackboard from a compact line DSL.
///
/// Supported examples:
///
/// * `root root`
/// * `selector root engage patrol`
/// * `condition see_enemy enemy_visible eq true`
/// * `action chase chase_enemy running,success mode=chasing`
/// * `blackboard ammo 3`
pub fn parse_dsl(
source : String,
options? : DslOptions,
) -> Result[DslDocument, BtError] {
let opts = options.unwrap_or(default_dsl_options())
let mut root = opts.default_root
let nodes = Array::new()
let board = new_blackboard()
let warnings = Array::new()
let lines = source.replace_all(old="\r\n", new="\n").split("\n").collect()
let mut line_no = 0
while line_no < lines.length() {
let raw_line = lines[line_no].to_owned()
let cleaned = if opts.allow_comments {
trim_ascii(strip_comment(raw_line))
} else {
trim_ascii(raw_line)
}
if cleaned != "" {
let tokens = split_ascii_words(cleaned)
if tokens.length() > 0 {
match parse_line(tokens, line_no + 1, board, warnings) {
Ok(LineRoot(id)) => root = id
Ok(LineNode(n)) => nodes.push(n)
Ok(LineNone) => ()
Err(err) => return Err(err)
}
}
}
line_no = line_no + 1
}
let tree = tree_from_nodes(root, nodes)
if opts.validate_after_parse {
let report = tree.validate()
if !report.ok {
return Err(InvalidTree(report.message()))
}
}
Ok({ tree, blackboard: board, warnings })
}
///|
pub fn parse_tree_dsl(source : String) -> Result[BehaviorTree, BtError] {
match parse_dsl(source) {
Ok(doc) => Ok(doc.tree)
Err(err) => Err(err)
}
}
///|
pub fn parse_blackboard_dsl(source : String) -> Result[Blackboard, BtError] {
let board = new_blackboard()
let lines = source.replace_all(old="\r\n", new="\n").split("\n").collect()
let mut i = 0
while i < lines.length() {
let line = trim_ascii(strip_comment(lines[i].to_owned()))
if line != "" {
let tokens = split_ascii_words(line)
if tokens.length() == 2 {
board.set(tokens[0], parse_value(tokens[1]))
} else if tokens.length() == 3 && tokens[0] == "blackboard" {
board.set(tokens[1], parse_value(tokens[2]))
} else {
return Err(ParseError("blackboard line " + (i + 1).to_string() + " is invalid"))
}
}
i = i + 1
}
Ok(board)
}
///|
pub fn serialize_tree(tree : BehaviorTree) -> Array[String] {
let lines = Array::new()
lines.push("root " + tree.root)
let mut i = 0
while i < tree.nodes.length() {
lines.push(serialize_node(tree.nodes[i]))
i = i + 1
}
lines
}
///|
pub fn serialize_blackboard(board : Blackboard) -> Array[String] {
let lines = Array::new()
let snapshot = board.snapshot()
let mut i = 0
while i < snapshot.length() {
lines.push("blackboard " + snapshot[i].0 + " " + quote_if_needed(snapshot[i].1.to_text()))
i = i + 1
}
lines
}
///|
enum ParsedLine {
LineRoot(String)
LineNode(BtNode)
LineNone
} derive(Debug, Eq)
///|
fn parse_line(
tokens : Array[String],
line_no : Int,
board : Blackboard,
warnings : Array[String],
) -> Result[ParsedLine, BtError] {
match tokens[0] {
"root" => {
if tokens.length() != 2 {
return Err(ParseError("line " + line_no.to_string() + ": root expects one id"))
}
Ok(LineRoot(tokens[1]))
}
"blackboard" => {
if tokens.length() != 3 {
return Err(ParseError("line " + line_no.to_string() + ": blackboard expects key value"))
}
board.set(tokens[1], parse_value(tokens[2]))
Ok(LineNone)
}
"sequence" => parse_composite(tokens, line_no, Sequence)
"selector" => parse_composite(tokens, line_no, Selector)
"parallel_all" => parse_composite(tokens, line_no, ParallelAll)
"parallel_any" => parse_composite(tokens, line_no, ParallelAny)
"inverter" => parse_decorator(tokens, line_no, Inverter)
"succeeder" => parse_decorator(tokens, line_no, Succeeder)
"failer" => parse_decorator(tokens, line_no, Failer)
"repeat" => parse_count_decorator(tokens, line_no, true)
"retry" => parse_count_decorator(tokens, line_no, false)
"condition" => parse_condition(tokens, line_no)
"set" => parse_set(tokens, line_no)
"wait" => parse_wait(tokens, line_no)
"emit" => parse_emit(tokens, line_no)
"action" => parse_action(tokens, line_no, warnings)
_ => Err(ParseError("line " + line_no.to_string() + ": unknown command " + tokens[0]))
}
}
///|
fn parse_composite(
tokens : Array[String],
line_no : Int,
kind : NodeKind,
) -> Result[ParsedLine, BtError] {
if tokens.length() < 3 {
return Err(
ParseError("line " + line_no.to_string() + ": composite expects id and children"),
)
}
let children = Array::new()
let mut i = 2
while i < tokens.length() {
children.push(tokens[i])
i = i + 1
}
Ok(LineNode(node(tokens[1], kind, children~)))
}
///|
fn parse_decorator(
tokens : Array[String],
line_no : Int,
kind : NodeKind,
) -> Result[ParsedLine, BtError] {
if tokens.length() != 3 {
return Err(
ParseError("line " + line_no.to_string() + ": decorator expects id and child"),
)
}
Ok(LineNode(node(tokens[1], kind, children=[tokens[2]])))
}
///|
fn parse_count_decorator(
tokens : Array[String],
line_no : Int,
repeat_kind : Bool,
) -> Result[ParsedLine, BtError] {
if tokens.length() != 4 {
return Err(
ParseError("line " + line_no.to_string() + ": count decorator expects id count child"),
)
}
match parse_positive_int(tokens[2]) {
Some(count) => {
let kind = if repeat_kind { Repeat(count) } else { Retry(count) }
Ok(LineNode(node(tokens[1], kind, children=[tokens[3]])))
}
None => Err(ParseError("line " + line_no.to_string() + ": invalid count " + tokens[2]))
}
}
///|
fn parse_condition(tokens : Array[String], line_no : Int) -> Result[ParsedLine, BtError] {
if tokens.length() != 5 {
return Err(
ParseError("line " + line_no.to_string() + ": condition expects id key op value"),
)
}
match parse_compare_op(tokens[3]) {
Some(op) => Ok(LineNode(condition(tokens[1], tokens[2], op, parse_value(tokens[4]))))
None => Err(ParseError("line " + line_no.to_string() + ": invalid compare op " + tokens[3]))
}
}
///|
fn parse_set(tokens : Array[String], line_no : Int) -> Result[ParsedLine, BtError] {
if tokens.length() != 4 {
return Err(ParseError("line " + line_no.to_string() + ": set expects id key value"))
}
Ok(LineNode(set_value(tokens[1], tokens[2], parse_value(tokens[3]))))
}
///|
fn parse_wait(tokens : Array[String], line_no : Int) -> Result[ParsedLine, BtError] {
if tokens.length() != 3 {
return Err(ParseError("line " + line_no.to_string() + ": wait expects id ticks"))
}
match parse_positive_int(tokens[2]) {
Some(ticks) => Ok(LineNode(wait(tokens[1], ticks)))
None => Err(ParseError("line " + line_no.to_string() + ": invalid wait ticks " + tokens[2]))
}
}
///|
fn parse_emit(tokens : Array[String], line_no : Int) -> Result[ParsedLine, BtError] {
if tokens.length() != 3 {
return Err(ParseError("line " + line_no.to_string() + ": emit expects id label"))
}
Ok(LineNode(node(tokens[1], Emit(unquote(tokens[2])))))
}
///|
fn parse_action(
tokens : Array[String],
line_no : Int,
warnings : Array[String],
) -> Result[ParsedLine, BtError] {
if tokens.length() < 4 {
return Err(
ParseError("line " + line_no.to_string() + ": action expects id name status-list"),
)
}
let statuses = parse_status_list(tokens[3])
if statuses.length() == 0 {
return Err(ParseError("line " + line_no.to_string() + ": action has empty status-list"))
}
let writes = Array::new()
let mut i = 4
while i < tokens.length() {
match parse_write_token(tokens[i]) {
Some(pair) => writes.push(pair)
None => warnings.push("line " + line_no.to_string() + ": ignored action token " + tokens[i])
}
i = i + 1
}
Ok(LineNode(node(tokens[1], ActionPlan(unquote(tokens[2]), statuses, writes))))
}
///|
fn parse_status_list(raw : String) -> Array[BtStatus] {
let out = Array::new()
let pieces = raw.split(",").collect()
let mut i = 0
while i < pieces.length() {
match parse_status(trim_ascii(pieces[i].to_owned())) {
Some(status) => out.push(status)
None => ()
}
i = i + 1
}
out
}
///|
fn parse_status(raw : String) -> BtStatus? {
match raw {
"success" | "ok" => Some(Success)
"failure" | "fail" => Some(Failure)
"running" | "run" => Some(Running)
_ => None
}
}
///|
fn parse_write_token(raw : String) -> (String, BtValue)? {
match raw.split_once("=") {
Some((left, right)) => {
let key = trim_ascii(left.to_owned())
if key == "" {
None
} else {
Some((key, parse_value(trim_ascii(right.to_owned()))))
}
}
None => None
}
}
///|
fn parse_positive_int(raw : String) -> Int? {
match parse_decimal_int(raw) {
Some(value) =>
if value >= 0 {
Some(value)
} else {
None
}
None => None
}
}
///|
fn serialize_node(n : BtNode) -> String {
match n.kind {
Sequence => "sequence " + n.id + " " + join_strings(n.children, " ")
Selector => "selector " + n.id + " " + join_strings(n.children, " ")
ParallelAll => "parallel_all " + n.id + " " + join_strings(n.children, " ")
ParallelAny => "parallel_any " + n.id + " " + join_strings(n.children, " ")
Inverter => "inverter " + n.id + " " + first_child(n)
Succeeder => "succeeder " + n.id + " " + first_child(n)
Failer => "failer " + n.id + " " + first_child(n)
Repeat(count) => "repeat " + n.id + " " + count.to_string() + " " + first_child(n)
Retry(count) => "retry " + n.id + " " + count.to_string() + " " + first_child(n)
Condition(key, op, value) =>
"condition " +
n.id +
" " +
key +
" " +
op.to_text() +
" " +
quote_if_needed(value.to_text())
SetValue(key, value) =>
"set " + n.id + " " + key + " " + quote_if_needed(value.to_text())
ActionPlan(name, statuses, writes) =>
"action " +
n.id +
" " +
quote_if_needed(name) +
" " +
serialize_statuses(statuses) +
serialize_writes(writes)
Wait(ticks) => "wait " + n.id + " " + ticks.to_string()
Emit(label) => "emit " + n.id + " " + quote_if_needed(label)
}
}
///|
fn serialize_statuses(statuses : Array[BtStatus]) -> String {
let out = Array::new()
let mut i = 0
while i < statuses.length() {
out.push(statuses[i].to_text())
i = i + 1
}
join_strings(out, ",")
}
///|
fn serialize_writes(writes : Array[(String, BtValue)]) -> String {
if writes.length() == 0 {
return ""
}
let out = Array::new()
let mut i = 0
while i < writes.length() {
out.push(writes[i].0 + "=" + quote_if_needed(writes[i].1.to_text()))
i = i + 1
}
" " + join_strings(out, " ")
}
///|
fn first_child(n : BtNode) -> String {
if n.children.length() == 0 {
""
} else {
n.children[0]
}
}
///|
fn quote_if_needed(raw : String) -> String {
if raw == "" {
"\"\""
} else if contains_space(raw) || raw.contains("#") {
"\"" + raw.replace_all(old="\"", new="\\\"") + "\""
} else {
raw
}
}
///|
fn strip_comment(raw : String) -> String {
let mut i = 0
let mut quoted = false
while i < raw.length() {
if raw[i] == 34 {
quoted = !quoted
} else if raw[i] == 35 && !quoted {
return raw.unsafe_substring(start=0, end=i)
}
i = i + 1
}
raw
}
///|
fn split_ascii_words(raw : String) -> Array[String] {
let out = Array::new()
let mut chars = Array::new()
let mut quoted = false
let mut escaped = false
let mut i = 0
while i < raw.length() {
let code = raw[i]
if escaped {
chars.push(code.unsafe_to_char())
escaped = false
} else if code == 92 && quoted {
escaped = true
} else if code == 34 {
quoted = !quoted
} else if is_ascii_space(code) && !quoted {
if chars.length() > 0 {
out.push(String::from_array(chars))
chars = Array::new()
}
} else {
chars.push(code.unsafe_to_char())
}
i = i + 1
}
if chars.length() > 0 || quoted {
out.push(String::from_array(chars))
}
out
}
///|
fn contains_space(raw : String) -> Bool {
let mut i = 0
while i < raw.length() {
if is_ascii_space(raw[i]) {
return true
}
i = i + 1
}
false
}