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