174 lines
5.0 KiB
Python
Executable File
174 lines
5.0 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Converts BehaviorTree.CPP V3 compatible tree xml files to V4 format.
|
|
"""
|
|
|
|
import argparse
|
|
import copy
|
|
import logging
|
|
import sys
|
|
import typing
|
|
import xml.etree.ElementTree as ET
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def strtobool(val: typing.Union[str, int, bool]) -> bool:
|
|
"""``distutils.util.strtobool`` equivalent, since it will be deprecated.
|
|
origin: https://stackoverflow.com/a/715468/17094594
|
|
"""
|
|
return str(val).lower() in ("yes", "true", "t", "1")
|
|
|
|
|
|
# see ``XMLParser::Pimpl::createNodeFromXML`` for all underscores
|
|
SCRIPT_DIRECTIVES = [
|
|
"_successIf",
|
|
"_failureIf",
|
|
"_skipIf",
|
|
"_while",
|
|
"_onSuccess",
|
|
"_onFailure",
|
|
"_onHalted",
|
|
"_post",
|
|
]
|
|
|
|
|
|
def convert_single_node(node: ET.Element) -> None:
|
|
"""converts a leaf node from V3 to V4.
|
|
Args:
|
|
node (ET.Element): the node to convert.
|
|
"""
|
|
if node.tag == "root":
|
|
node.attrib["BTCPP_format"] = "4"
|
|
|
|
def convert_no_warn(node_type: str, v3_name: str, v4_name: str):
|
|
if node.tag == v3_name:
|
|
node.tag = v4_name
|
|
elif (
|
|
(node.tag == node_type)
|
|
and ("ID" in node.attrib)
|
|
and (node.attrib["ID"] == v3_name)
|
|
):
|
|
node.attrib["ID"] = v3_name
|
|
|
|
original_attrib = copy.copy(node.attrib)
|
|
convert_no_warn("Control", "SequenceStar", "SequenceWithMemory")
|
|
|
|
if node.tag == "SubTree":
|
|
logger.info(
|
|
"SubTree is now deprecated, auto converting to V4 SubTree"
|
|
" (formerly known as SubTreePlus)"
|
|
)
|
|
for key, val in original_attrib.items():
|
|
if key == "__shared_blackboard" and strtobool(val):
|
|
logger.warning(
|
|
"__shared_blackboard for subtree is deprecated"
|
|
", using _autoremap instead."
|
|
" Some behavior may change!"
|
|
)
|
|
node.attrib.pop(key)
|
|
node.attrib["_autoremap"] = "1"
|
|
elif key == "ID":
|
|
pass
|
|
else:
|
|
node.attrib[key] = f"{{{val}}}"
|
|
|
|
elif node.tag == "SubTreePlus":
|
|
node.tag = "SubTree"
|
|
for key, val in original_attrib.items():
|
|
if key == "__autoremap":
|
|
node.attrib.pop(key)
|
|
node.attrib["_autoremap"] = val
|
|
|
|
for key in node.attrib:
|
|
if key in SCRIPT_DIRECTIVES:
|
|
logging.error(
|
|
"node %s%s has port %s, this is reserved for scripts in V4."
|
|
" Please edit the node before converting to V4.",
|
|
node.tag,
|
|
f" with ID {node.attrib['ID']}" if "ID" in node.attrib else "",
|
|
key,
|
|
)
|
|
|
|
|
|
def convert_all_nodes(root_node: ET.Element) -> None:
|
|
"""recursively converts all nodes inside a root node.
|
|
Args:
|
|
root_node (ET.Element): the root node to start the conversion.
|
|
"""
|
|
|
|
def recurse(base_node: ET.Element) -> None:
|
|
convert_single_node(base_node)
|
|
for node in base_node:
|
|
recurse(node)
|
|
|
|
recurse(root_node)
|
|
|
|
|
|
def convert_stream(in_stream: typing.TextIO, out_stream: typing.TextIO):
|
|
"""Converts the behavior tree V3 xml from in_file to V4, and writes to out_file.
|
|
Args:
|
|
in_stream (typing.TextIO): The input file stream.
|
|
out_stream (typing.TextIO): The output file stream.
|
|
"""
|
|
|
|
class CommentedTreeBuilder(ET.TreeBuilder):
|
|
"""Class for preserving comments in xml
|
|
see: https://stackoverflow.com/a/34324359/17094594
|
|
"""
|
|
|
|
def comment(self, text):
|
|
self.start(ET.Comment, {})
|
|
self.data(text)
|
|
self.end(ET.Comment)
|
|
|
|
element_tree = ET.parse(in_stream, ET.XMLParser(target=CommentedTreeBuilder()))
|
|
convert_all_nodes(element_tree.getroot())
|
|
element_tree.write(out_stream, encoding="unicode", xml_declaration=True)
|
|
|
|
|
|
def main():
|
|
"""the main function when used in cli mode"""
|
|
|
|
logger.addHandler(logging.StreamHandler())
|
|
logger.setLevel(logging.DEBUG)
|
|
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument(
|
|
"-i",
|
|
"--in_file",
|
|
type=argparse.FileType("r"),
|
|
help="The file to convert from (v3). If absent, reads xml string from stdin.",
|
|
)
|
|
parser.add_argument(
|
|
"-o",
|
|
"--out_file",
|
|
nargs="?",
|
|
type=argparse.FileType("w"),
|
|
default=sys.stdout,
|
|
help="The file to write the converted xml (V4)."
|
|
" Prints to stdout if not specified.",
|
|
)
|
|
|
|
class ArgsType(typing.NamedTuple):
|
|
"""Dummy class to provide type hinting to arguments parsed with argparse"""
|
|
|
|
in_file: typing.Optional[typing.TextIO]
|
|
out_file: typing.TextIO
|
|
|
|
args: ArgsType = parser.parse_args()
|
|
|
|
if args.in_file is None:
|
|
if not sys.stdin.isatty():
|
|
args.in_file = sys.stdin
|
|
else:
|
|
logging.error(
|
|
"The input file was not specified, nor a stdin stream was detected."
|
|
)
|
|
sys.exit(1)
|
|
|
|
convert_stream(args.in_file, args.out_file)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|