///|
/// Compatibility result for two versions of a behavior tree asset.
pub(all) enum CompatibilityLevel {
Compatible
ReviewRequired
Breaking
} derive(Debug, Eq)
///|
pub fn CompatibilityLevel::to_text(self : CompatibilityLevel) -> String {
match self {
Compatible => "compatible"
ReviewRequired => "review-required"
Breaking => "breaking"
}
}
///|
/// A structural change for one node that exists in both tree versions.
pub(all) struct NodeDelta {
id : String
before_kind : String
after_kind : String
kind_changed : Bool
children_changed : Bool
name_changed : Bool
} derive(Debug, Eq)
///|
pub fn NodeDelta::is_breaking(self : NodeDelta) -> Bool {
self.kind_changed || self.children_changed
}
///|
pub fn NodeDelta::summary(self : NodeDelta) -> String {
let changes = Array::new()
if self.kind_changed {
changes.push("kind " + self.before_kind + " -> " + self.after_kind)
}
if self.children_changed {
changes.push("children changed")
}
if self.name_changed {
changes.push("name changed")
}
self.id + ": " + join_strings(changes, ", ")
}
///|
/// Structural difference between two behavior tree assets.
pub(all) struct TreeDiff {
before_root : String
after_root : String
added_nodes : Array[String]
removed_nodes : Array[String]
changed_nodes : Array[NodeDelta]
} derive(Debug, Eq)
///|
pub fn TreeDiff::root_changed(self : TreeDiff) -> Bool {
self.before_root != self.after_root
}
///|
pub fn TreeDiff::change_count(self : TreeDiff) -> Int {
self.added_nodes.length() +
self.removed_nodes.length() +
self.changed_nodes.length() +
(if self.root_changed() { 1 } else { 0 })
}
///|
pub fn TreeDiff::is_empty(self : TreeDiff) -> Bool {
self.change_count() == 0
}
///|
pub fn TreeDiff::compatibility(self : TreeDiff) -> CompatibilityLevel {
if self.root_changed() || self.removed_nodes.length() > 0 {
return Breaking
}
let mut i = 0
while i < self.changed_nodes.length() {
if self.changed_nodes[i].is_breaking() {
return Breaking
}
i = i + 1
}
if self.changed_nodes.length() > 0 {
ReviewRequired
} else {
Compatible
}
}
///|
pub fn TreeDiff::summary(self : TreeDiff) -> String {
"compatibility=" +
self.compatibility().to_text() +
", changes=" +
self.change_count().to_string() +
", added=" +
self.added_nodes.length().to_string() +
", removed=" +
self.removed_nodes.length().to_string() +
", changed=" +
self.changed_nodes.length().to_string()
}
///|
pub fn TreeDiff::lines(self : TreeDiff) -> Array[String] {
let lines = Array::new()
lines.push(self.summary())
if self.root_changed() {
lines.push("root: " + self.before_root + " -> " + self.after_root)
}
if self.added_nodes.length() > 0 {
lines.push("added: " + join_strings(self.added_nodes, ", "))
}
if self.removed_nodes.length() > 0 {
lines.push("removed: " + join_strings(self.removed_nodes, ", "))
}
let mut i = 0
while i < self.changed_nodes.length() {
lines.push("changed: " + self.changed_nodes[i].summary())
i = i + 1
}
lines
}
///|
pub fn TreeDiff::markdown(self : TreeDiff, title? : String) -> String {
let lines = Array::new()
lines.push("# " + title.unwrap_or("MoonBTKit Asset Diff"))
lines.push("")
lines.push("- " + self.summary())
lines.push("")
lines.push("## Changes")
lines.push("")
let details = self.lines()
let mut i = 1
if details.length() == 1 {
lines.push("- no structural changes")
}
while i < details.length() {
lines.push("- " + details[i])
i = i + 1
}
join_strings(lines, "\n")
}
///|
/// Compare two validated behavior tree models by stable node identifiers.
pub fn diff_trees(before : BehaviorTree, after : BehaviorTree) -> TreeDiff {
let added = Array::new()
let removed = Array::new()
let changed = Array::new()
let mut i = 0
while i < before.nodes.length() {
let old_node = before.nodes[i]
match after.node(old_node.id) {
Some(new_node) => {
let kind_changed = old_node.kind != new_node.kind
let children_changed = !same_string_array(
old_node.children,
new_node.children,
)
let name_changed = old_node.name != new_node.name
if kind_changed || children_changed || name_changed {
changed.push({
id: old_node.id,
before_kind: old_node.kind.to_text(),
after_kind: new_node.kind.to_text(),
kind_changed,
children_changed,
name_changed,
})
}
}
None => removed.push(old_node.id)
}
i = i + 1
}
let mut j = 0
while j < after.nodes.length() {
if !before.has_node(after.nodes[j].id) {
added.push(after.nodes[j].id)
}
j = j + 1
}
{
before_root: before.root,
after_root: after.root,
added_nodes: added,
removed_nodes: removed,
changed_nodes: changed,
}
}
///|
/// Parse and compare two DSL assets. Invalid assets return their parse error.
pub fn diff_dsl(
before_source : String,
after_source : String,
) -> Result[TreeDiff, BtError] {
match parse_tree_dsl(before_source) {
Ok(before) =>
match parse_tree_dsl(after_source) {
Ok(after) => Ok(diff_trees(before, after))
Err(err) => Err(err)
}
Err(err) => Err(err)
}
}
///|
fn same_string_array(left : Array[String], right : Array[String]) -> Bool {
if left.length() != right.length() {
return false
}
let mut i = 0
while i < left.length() {
if left[i] != right[i] {
return false
}
i = i + 1
}
true
}