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