Skip to content

Commit e9f4980

Browse files
committedNov 19, 2022
add XML converter
1 parent 1a4bb0e commit e9f4980

File tree

1 file changed

+173
-0
lines changed

1 file changed

+173
-0
lines changed
 

‎convert_v3_to_v4.py

+173
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
#!/usr/bin/env python3
2+
"""Converts BehaviorTree.CPP V3 compatible tree xml files to V4 format.
3+
"""
4+
5+
import argparse
6+
import copy
7+
import logging
8+
import sys
9+
import typing
10+
import xml.etree.ElementTree as ET
11+
12+
logger = logging.getLogger(__name__)
13+
14+
15+
def strtobool(val: typing.Union[str, int, bool]) -> bool:
16+
"""``distutils.util.strtobool`` equivalent, since it will be deprecated.
17+
origin: https://stackoverflow.com/a/715468/17094594
18+
"""
19+
return str(val).lower() in ("yes", "true", "t", "1")
20+
21+
22+
# see ``XMLParser::Pimpl::createNodeFromXML`` for all underscores
23+
SCRIPT_DIRECTIVES = [
24+
"_successIf",
25+
"_failureIf",
26+
"_skipIf",
27+
"_while",
28+
"_onSuccess",
29+
"_onFailure",
30+
"_onHalted",
31+
"_post",
32+
]
33+
34+
35+
def convert_single_node(node: ET.Element) -> None:
36+
"""converts a leaf node from V3 to V4.
37+
Args:
38+
node (ET.Element): the node to convert.
39+
"""
40+
if node.tag == "root":
41+
node.attrib["BTCPP_format"] = "4"
42+
43+
def convert_no_warn(node_type: str, v3_name: str, v4_name: str):
44+
if node.tag == v3_name:
45+
node.tag = v4_name
46+
elif (
47+
(node.tag == node_type)
48+
and ("ID" in node.attrib)
49+
and (node.attrib["ID"] == v3_name)
50+
):
51+
node.attrib["ID"] = v3_name
52+
53+
original_attrib = copy.copy(node.attrib)
54+
convert_no_warn("Control", "SequenceStar", "SequenceWithMemory")
55+
56+
if node.tag == "SubTree":
57+
logger.info(
58+
"SubTree is now deprecated, auto converting to V4 SubTree"
59+
" (formerly known as SubTreePlus)"
60+
)
61+
for key, val in original_attrib.items():
62+
if key == "__shared_blackboard" and strtobool(val):
63+
logger.warning(
64+
"__shared_blackboard for subtree is deprecated"
65+
", using _autoremap instead."
66+
" Some behavior may change!"
67+
)
68+
node.attrib.pop(key)
69+
node.attrib["_autoremap"] = "1"
70+
elif key == "ID":
71+
pass
72+
else:
73+
node.attrib[key] = f"{{{val}}}"
74+
75+
elif node.tag == "SubTreePlus":
76+
node.tag = "SubTree"
77+
for key, val in original_attrib.items():
78+
if key == "__autoremap":
79+
node.attrib.pop(key)
80+
node.attrib["_autoremap"] = val
81+
82+
for key in node.attrib:
83+
if key in SCRIPT_DIRECTIVES:
84+
logging.error(
85+
"node %s%s has port %s, this is reserved for scripts in V4."
86+
" Please edit the node before converting to V4.",
87+
node.tag,
88+
f" with ID {node.attrib['ID']}" if "ID" in node.attrib else "",
89+
key,
90+
)
91+
92+
93+
def convert_all_nodes(root_node: ET.Element) -> None:
94+
"""recursively converts all nodes inside a root node.
95+
Args:
96+
root_node (ET.Element): the root node to start the conversion.
97+
"""
98+
99+
def recurse(base_node: ET.Element) -> None:
100+
convert_single_node(base_node)
101+
for node in base_node:
102+
recurse(node)
103+
104+
recurse(root_node)
105+
106+
107+
def convert_stream(in_stream: typing.TextIO, out_stream: typing.TextIO):
108+
"""Converts the behavior tree V3 xml from in_file to V4, and writes to out_file.
109+
Args:
110+
in_stream (typing.TextIO): The input file stream.
111+
out_stream (typing.TextIO): The output file stream.
112+
"""
113+
114+
class CommentedTreeBuilder(ET.TreeBuilder):
115+
"""Class for preserving comments in xml
116+
see: https://stackoverflow.com/a/34324359/17094594
117+
"""
118+
119+
def comment(self, text):
120+
self.start(ET.Comment, {})
121+
self.data(text)
122+
self.end(ET.Comment)
123+
124+
element_tree = ET.parse(in_stream, ET.XMLParser(target=CommentedTreeBuilder()))
125+
convert_all_nodes(element_tree.getroot())
126+
element_tree.write(out_stream, encoding="unicode", xml_declaration=True)
127+
128+
129+
def main():
130+
"""the main function when used in cli mode"""
131+
132+
logger.addHandler(logging.StreamHandler())
133+
logger.setLevel(logging.DEBUG)
134+
135+
parser = argparse.ArgumentParser(description=__doc__)
136+
parser.add_argument(
137+
"-i",
138+
"--in_file",
139+
type=argparse.FileType("r"),
140+
help="The file to convert from (v3). If absent, reads xml string from stdin.",
141+
)
142+
parser.add_argument(
143+
"-o",
144+
"--out_file",
145+
nargs="?",
146+
type=argparse.FileType("w"),
147+
default=sys.stdout,
148+
help="The file to write the converted xml (V4)."
149+
" Prints to stdout if not specified.",
150+
)
151+
152+
class ArgsType(typing.NamedTuple):
153+
"""Dummy class to provide type hinting to arguments parsed with argparse"""
154+
155+
in_file: typing.Optional[typing.TextIO]
156+
out_file: typing.TextIO
157+
158+
args: ArgsType = parser.parse_args()
159+
160+
if args.in_file is None:
161+
if not sys.stdin.isatty():
162+
args.in_file = sys.stdin
163+
else:
164+
logging.error(
165+
"The input file was not specified, nor a stdin stream was detected."
166+
)
167+
sys.exit(1)
168+
169+
convert_stream(args.in_file, args.out_file)
170+
171+
172+
if __name__ == "__main__":
173+
main()

0 commit comments

Comments
 (0)