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