from typing import Any, Dict, List, Union
from UnityPy.streams import EndianBinaryReader, EndianBinaryWriter
from ctypes import c_uint32
import tabulate
from ..exceptions import TypeTreeError as TypeTreeError
class TypeTreeNode(object):
type: str
name: str
byte_size: int
index: int
is_array: int
version: int
meta_flag: int
level: int
type_str_offset: int
name_str_offset: int
def __init__(self, data: dict = None):
if data:
self.__dict__.update(data)
def __repr__(self):
return f""
def node_dict_to_class(nodes: List[dict]) -> List[TypeTreeNode]:
"""Converts all dict-type nodes into TypeTreeNodes
Parameters
----------
nodes : List[dict]
nodes/nodes of the typetree as dict
Returns
-------
List[TypeTreeNode]
a list of TypeTreeNode-type nodes
"""
return [TypeTreeNode(node) for node in nodes]
def check_nodes(nodes: List[Union[dict, TypeTreeNode]]) -> List[TypeTreeNode]:
"""Checks the type of the nodes and converts them if necessary.
Parameters
----------
nodes : List[Union[dict, TypeTreeNode]]
nodes/nodes of the typetree as dict or TypeTreeNode
Returns
-------
List[TypeTreeNode]
a list of TypeTreeNode-type nodes
"""
if isinstance(nodes, list):
if len(nodes) == 0:
raise ValueError("not enough nodes")
if isinstance(nodes[0], TypeTreeNode):
return nodes
elif isinstance(nodes[0], dict):
return node_dict_to_class(nodes)
raise ValueError(
f"nodes must be a list of dict or TypeTreeNode elements, but received {type(nodes)} - {type(nodes[0]) if isinstance(nodes, list) else ''}"
)
"""
Example Nodes:
[{
"level": 0,
"type": "MonoBehaviour",
"name": "Base",
"meta_flag": 0
},
{
"level": 1,
"type": "int",
"name": "m_SomeNode",
"meta_flag": 0
}]
Example TypeTree:
{
"m_SomeNode": 1
}
"""
def get_nodes(nodes: List[TypeTreeNode], index: int) -> list:
"""Copies all nodes above the level of the node at the set index.
Parameters
----------
nodes : list
nodes/nodes of the typetree
index : int
index of the node
Returns
-------
list
A list of nodes
"""
nodes2 = [nodes[index]]
level = nodes[index].level
for node in nodes[index + 1 :]:
if node.level <= level:
return nodes2
nodes2.append(node)
return nodes2
def read_typetree(
nodes: List[Union[dict, TypeTreeNode]], reader: EndianBinaryReader
) -> dict:
"""Reads the typetree of the object contained in the reader via the node list.
Parameters
----------
nodes : list
List of nodes/nodes
reader : EndianBinaryReader
Reader of the object to be parsed
Returns
-------
dict
The parsed typtree
"""
# reader.reset()
nodes = check_nodes(nodes)
obj = {}
i = c_uint32(1)
while i.value < len(nodes):
node = nodes[i.value]
obj[node.name] = read_value(nodes, reader, i)
i.value += 1
readed = reader.Position - reader.byte_start
if readed != reader.byte_size:
raise TypeTreeError(
f"Error while read type, read {readed} bytes but expected {reader.byte_size} bytes",
nodes,
)
return obj
def read_value(nodes: List[TypeTreeNode], reader: EndianBinaryReader, i: c_uint32):
node = nodes[i.value]
typ = node.type
align = (node.meta_flag & 0x4000) != 0
if typ == "SInt8":
value = reader.read_byte()
elif typ in ["UInt8", "char"]:
value = reader.read_u_byte()
elif typ in ["short", "SInt16"]:
value = reader.read_short()
elif typ in ["UInt16", "unsigned short"]:
value = reader.read_u_short()
elif typ in ["int", "SInt32"]:
value = reader.read_int()
elif typ in ["UInt32", "unsigned int", "Type*"]:
value = reader.read_u_int()
elif typ in ["long long", "SInt64"]:
value = reader.read_long()
elif typ in ["UInt64", "unsigned long long", "FileSize"]:
value = reader.read_u_long()
elif typ == "float":
value = reader.read_float()
elif typ == "double":
value = reader.read_double()
elif typ == "bool":
value = reader.read_boolean()
elif typ == "string":
value = reader.read_aligned_string()
i.value += 3 # Array, Size, Data(typ)
elif typ == "map": # map == MultiDict
if (nodes[i.value + 1].meta_flag & 0x4000) != 0:
align = True
map_ = get_nodes(nodes, i.value)
i.value += len(map_) - 1
first = get_nodes(map_, 4)
second = get_nodes(map_, 4 + len(first))
size = reader.read_int()
value = [None] * size
for j in range(size):
key = read_value(first, reader, c_uint32(0))
value[j] = (key, read_value(second, reader, c_uint32(0)))
elif typ == "TypelessData":
size = reader.read_int()
value = reader.read_bytes(size)
i.value += 2 # Size == int, Data(typ) == char/uint8
else:
# Vector
if i.value < len(nodes) - 1 and nodes[i.value + 1].type == "Array":
if (nodes[i.value + 1].meta_flag & 0x4000) != 0:
align = True
vector = get_nodes(nodes, i.value)
i.value += len(vector) - 1
size = reader.read_int()
value = [read_value(vector, reader, c_uint32(3)) for _ in range(size)]
else: # Class
clz = get_nodes(nodes, i.value)
i.value += len(clz) - 1
value = {}
j = c_uint32(1)
while j.value < len(clz):
clz_node = clz[j.value]
value[clz_node.name] = read_value(clz, reader, j)
j.value += 1
if align:
reader.align_stream()
return value
def read_typetree_str(
sb: List[str], nodes: List[Union[dict, TypeTreeNode]], reader: EndianBinaryReader
) -> list:
"""Reads the typetree of the object contained in the reader via the node list and dumps it as string.
Parameters
----------
sb : list
StringBuilder - a list used to build the string dump, should be empty
nodes : list
List of nodes/nodes
reader : EndianBinaryReader
Reader of the object to be parsed
Returns
-------
list
The sb given as input
"""
# reader.reset()
nodes = check_nodes(nodes)
i = c_uint32(0)
while i.value < len(nodes):
read_value_str(sb, nodes, reader, i)
i.value += 1
readed = reader.Position - reader.byte_start
if readed != reader.byte_size:
raise TypeTreeError(
f"Error while read type, read {readed} bytes but expected {reader.byte_size} bytes",
nodes,
)
return sb
def read_value_str(
sb: List[str], nodes: List[TypeTreeNode], reader: EndianBinaryReader, i: c_uint32
) -> list:
node = nodes[i.value]
typ = node.type
align = (node.meta_flag & 0x4000) != 0
append = True
if typ == "SInt8":
value = reader.read_byte()
elif typ in ["UInt8", "char"]:
value = reader.read_u_byte()
elif typ in ["short", "SInt16"]:
value = reader.read_short()
elif typ in ["UInt16", "unsigned short"]:
value = reader.read_u_short()
elif typ in ["int", "SInt32"]:
value = reader.read_int()
elif typ in ["UInt32", "unsigned int", "Type*"]:
value = reader.read_u_int()
elif typ in ["long long", "SInt64"]:
value = reader.read_long()
elif typ in ["UInt64", "unsigned long long", "FileSize"]:
value = reader.read_u_long()
elif typ == "float":
value = reader.read_float()
elif typ == "double":
value = reader.read_double()
elif typ == "bool":
value = reader.read_boolean()
elif typ == "string":
value = reader.read_aligned_string()
i.value += 3 # Array, Size, Data(typ)
append = False
sb.append(
'{0}{1} {2} = "{3}"\r\n'.format(
"\t" * node.level, node.type, node.name, value
)
)
elif typ == "map":
if (nodes[i.value + 1].meta_flag & 0x4000) != 0:
align = True
map_ = get_nodes(nodes, i.value)
i.value += len(map_) - 1
first = get_nodes(map_, 4)
second = get_nodes(map_, 4 + len(first))
size = reader.read_int()
append = False
sb.append("{0}{1} {2}\r\n".format("\t" * node.level, node.type, node.name))
sb.append("{0}{1} {2}\r\n".format("\t" * (node.level + 1), "Array", "Array"))
sb.append(
"{0}{1} {2} = {3}\r\n".format("\t" * (node.level + 1), "int", "size", size)
)
for j in range(size):
sb.append("{0}[{1}]\r\n".format("\t" * (node.level + 2), j))
sb.append("{0}{1} {2}\r\n".format("\t" * (node.level + 2), "pair", "data"))
read_value_str(sb, first, reader, c_uint32(0))
read_value_str(sb, second, reader, c_uint32(0))
elif typ == "TypelessData":
size = reader.read_int()
value = reader.read_bytes(size)
i.value += 2 # Size == int, Data(typ) == char/uint8
append = False
sb.append("{0}{1} {2}\r\n".format("\t" * node.level, node.type, node.name))
sb.append("{0}{1} {2} = {3}\r\n".format("\t" * node.level, "int", "size", size))
# sb.append("{0}{1} {2} = {3}\r\n".format(
# "\t" * node.level, "UInt8", "data", base64.b64encode(value)))
else:
# Vector
if i.value < len(nodes) - 1 and nodes[i.value + 1].type == "Array":
if (nodes[i.value + 1].meta_flag & 0x4000) != 0:
align = True
vector = get_nodes(nodes, i.value)
i.value += len(vector) - 1
size = reader.read_int()
append = False
sb.append("{0}{1} {2}\r\n".format("\t" * node.level, node.type, node.name))
sb.append(
"{0}{1} {2}\r\n".format("\t" * (node.level + 1), "Array", "Array")
)
sb.append(
"{0}{1} {2} = {3}\r\n".format(
"\t" * (node.level + 1), "int", "size", size
)
)
for j in range(size):
sb.append("{0}[{1}]\r\n".format("\t" * (node.level + 2), j))
read_value_str(sb, vector, reader, c_uint32(3))
else: # Class
clz = get_nodes(nodes, i.value)
i.value += len(clz) - 1
j = c_uint32(1)
append = False
sb.append("{0}{1} {2}\r\n".format("\t" * node.level, node.type, node.name))
while j.value < len(clz):
read_value_str(sb, clz, reader, j)
j.value += 1
if append:
sb.append(
"{0}{1} {2} = {3}\r\n".format(
"\t" * node.level, node.type, node.name, value
)
)
if align:
reader.align_stream()
return sb
def dump_typetree(nodes: List[TypeTreeNode]) -> str:
"""Dumps the structure of the given nodes.
Parameters
----------
nodes : list
List of nodes/nodes
Returns
-------
str
The dumped structure
"""
field_names = ["level", "type", "name", "meta_flag"]
rows = [[getattr(x, key) for key in field_names] for x in nodes]
return tabulate.tabulate(rows, headers=field_names)
def write_typetree(
obj: dict, nodes: List[Union[dict, TypeTreeNode]], writer: EndianBinaryWriter = None
) -> EndianBinaryWriter:
"""Writes the data of the object via the given typetree of the object into the writer.
Parameters
----------
obj : dict
Object to be saved
nodes : list
List of nodes/nodes
writer : EndianBinaryWriter
Writer of the object to be saved
Returns
-------
EndianBinaryWriter
The writer that was used to save the data of the given object.
"""
if not writer:
writer = EndianBinaryWriter()
nodes = check_nodes(nodes)
i = c_uint32(1)
while i.value < len(nodes):
value = obj[nodes[i.value].name]
write_value(value, nodes, writer, i)
i.value += 1
return writer
def write_value(
value: Any, nodes: List[TypeTreeNode], writer: EndianBinaryWriter, i: c_uint32
):
node = nodes[i.value]
typ = node.type
align = (node.meta_flag & 0x4000) != 0
if typ == "SInt8":
writer.write_byte(value)
elif typ in ["UInt8", "char"]:
writer.write_u_byte(value)
elif typ in ["short", "SInt16"]:
writer.write_short(value)
elif typ in ["UInt16", "unsigned short"]:
writer.write_u_short(value)
elif typ in ["int", "SInt32"]:
writer.write_int(value)
elif typ in ["UInt32", "unsigned int", "Type*"]:
writer.write_u_int(value)
elif typ in ["long long", "SInt64"]:
writer.write_long(value)
elif typ in ["UInt64", "unsigned long long", "FileSize"]:
writer.write_u_long(value)
elif typ == "float":
writer.write_float(value)
elif typ == "double":
writer.write_double(value)
elif typ == "bool":
writer.write_boolean(value)
elif typ == "string":
writer.write_aligned_string(value)
i.value += 3 # Array, Size, Data(typ)
elif typ == "map":
if (nodes[i.value + 1].meta_flag & 0x4000) != 0:
align = True
map_ = get_nodes(nodes, i.value)
i.value += len(map_) - 1
first = get_nodes(map_, 4)
second = get_nodes(map_, 4 + len(first))
# size
writer.write_int(len(value))
# data
for key, val in value:
write_value(key, first, writer, c_uint32(0))
write_value(val, second, writer, c_uint32(0))
elif typ == "TypelessData":
writer.write_int(len(value))
writer.write_bytes(value)
i.value += 2 # Size == int, Data(typ) == char/uint8
else:
# Vector
if i.value < len(nodes) - 1 and nodes[i.value + 1].type == "Array":
if (nodes[i.value + 1].meta_flag & 0x4000) != 0:
align = True
vector = get_nodes(nodes, i.value)
i.value += len(vector) - 1
writer.write_int(len(value))
for val in value:
write_value(val, vector, writer, c_uint32(3))
else: # Class
clz = get_nodes(nodes, i.value)
i.value += len(clz) - 1
j = c_uint32(1)
while j.value < len(clz):
val = value[clz[j.value].name]
write_value(val, clz, writer, j)
j.value += 1
if align:
writer.align_stream()