make_icon_3db, make_image_3db and make_hud_txm with their helpers, standalone (the name CRC table is in flcrc.py). The .gitignore keeps crash dumps, logs, traces, debug symbols and everything of the server out of the mod.
152 lines
4.8 KiB
Python
152 lines
4.8 KiB
Python
"""Freelancer UTF container (.3db, .cmp, .mat, .txm): read, edit and write.
|
|
|
|
A UTF file is a tree of named nodes; a directory node has children, a leaf node carries bytes.
|
|
Header (0x38 bytes): "UTF ", version 0x101, tree offset/size, node size 44, string table
|
|
offset/allocated/used, data offset. Node (44 bytes): next sibling offset, name offset, flags
|
|
(0x10 directory, 0x80 leaf), 0, child offset (directory) or data offset (leaf), allocated,
|
|
size, size again, three FILETIME words.
|
|
|
|
python tools/utf.py dump file.3db
|
|
"""
|
|
import struct
|
|
import sys
|
|
|
|
|
|
class Node:
|
|
def __init__(self, name, data=None, children=None):
|
|
self.name = name
|
|
self.data = data # bytes for a leaf
|
|
self.children = children # list for a directory
|
|
|
|
@property
|
|
def is_dir(self):
|
|
return self.children is not None
|
|
|
|
def find(self, path):
|
|
"""The node at a '\\'-separated path below this one (case-insensitive), or None."""
|
|
node = self
|
|
for part in [p for p in path.replace('/', '\\').split('\\') if p]:
|
|
if not node.is_dir:
|
|
return None
|
|
nxt = None
|
|
for c in node.children:
|
|
if c.name.lower() == part.lower():
|
|
nxt = c
|
|
break
|
|
if nxt is None:
|
|
return None
|
|
node = nxt
|
|
return node
|
|
|
|
def walk(self, prefix=''):
|
|
path = prefix + '\\' + self.name if prefix else self.name
|
|
yield path, self
|
|
if self.is_dir:
|
|
for c in self.children:
|
|
yield from c.walk(path)
|
|
|
|
|
|
def read(path):
|
|
raw = open(path, 'rb').read()
|
|
sig, ver, tree_ofs, tree_size, _res, node_size, str_ofs, str_alloc, str_used, data_ofs = \
|
|
struct.unpack_from('<4sIIIIIIIII', raw, 0)
|
|
if sig != b'UTF ':
|
|
raise ValueError(path + ': not a UTF file')
|
|
strings = raw[str_ofs:str_ofs + str_alloc]
|
|
|
|
def name_at(o):
|
|
end = strings.index(b'\0', o)
|
|
return strings[o:end].decode('latin-1')
|
|
|
|
def node_at(o):
|
|
nxt, name_o, flags, _z, ptr, alloc, size, _size2 = struct.unpack_from('<8I', raw, tree_ofs + o)
|
|
name = name_at(name_o)
|
|
if flags & 0x10:
|
|
children = []
|
|
c = ptr
|
|
while True:
|
|
child, c_next = node_at(c)
|
|
children.append(child)
|
|
if c_next == 0:
|
|
break
|
|
c = c_next
|
|
return Node(name, children=children), nxt
|
|
return Node(name, data=raw[data_ofs + ptr:data_ofs + ptr + size]), nxt
|
|
|
|
root, _ = node_at(0)
|
|
return root
|
|
|
|
|
|
def write(path, root):
|
|
"""Writes the tree: nodes in depth-first order, names deduplicated, data 4-byte aligned."""
|
|
nodes = []
|
|
for _, n in root.walk():
|
|
nodes.append(n)
|
|
index = {id(n): i for i, n in enumerate(nodes)}
|
|
|
|
names = bytearray(b'\0')
|
|
name_ofs = {}
|
|
for n in nodes:
|
|
if n.name not in name_ofs:
|
|
name_ofs[n.name] = len(names)
|
|
names += n.name.encode('latin-1') + b'\0'
|
|
while len(names) % 4:
|
|
names += b'\0'
|
|
|
|
data = bytearray()
|
|
data_ofs = {}
|
|
for n in nodes:
|
|
if not n.is_dir:
|
|
data_ofs[id(n)] = len(data)
|
|
data += n.data
|
|
while len(data) % 4:
|
|
data += b'\0'
|
|
|
|
# the next sibling of each node
|
|
sibling = {}
|
|
for n in nodes:
|
|
if n.is_dir:
|
|
for a, b in zip(n.children, n.children[1:]):
|
|
sibling[id(a)] = b
|
|
|
|
node_size = 44
|
|
tree = bytearray()
|
|
for n in nodes:
|
|
nxt = sibling.get(id(n))
|
|
nxt_o = index[id(nxt)] * node_size if nxt is not None else 0
|
|
if n.is_dir:
|
|
child_o = index[id(n.children[0])] * node_size if n.children else 0
|
|
tree += struct.pack('<8I3I', nxt_o, name_ofs[n.name], 0x10, 0, child_o, 0, 0, 0, 0, 0, 0)
|
|
else:
|
|
size = len(n.data)
|
|
alloc = (size + 3) & ~3
|
|
tree += struct.pack('<8I3I', nxt_o, name_ofs[n.name], 0x80, 0, data_ofs[id(n)], alloc, size, size,
|
|
0, 0, 0)
|
|
|
|
header_size = 0x38
|
|
tree_ofs = header_size
|
|
str_ofs = tree_ofs + len(tree)
|
|
dat_ofs = str_ofs + len(names)
|
|
header = struct.pack('<4sIIIIIIIIIII', b'UTF ', 0x101, tree_ofs, len(tree), 0, node_size, str_ofs, len(names),
|
|
len(names), dat_ofs, 0, 0) + b'\0' * 8
|
|
open(path, 'wb').write(header + tree + names + data)
|
|
|
|
|
|
def dump(path):
|
|
root = read(path)
|
|
for p, n in root.walk():
|
|
if n.is_dir:
|
|
print(p + '\\')
|
|
else:
|
|
preview = n.data[:24].hex(' ')
|
|
print('%s [%d] %s' % (p, len(n.data), preview))
|
|
|
|
|
|
if __name__ == '__main__':
|
|
if len(sys.argv) >= 3 and sys.argv[1] == 'dump':
|
|
for f in sys.argv[2:]:
|
|
print('==', f)
|
|
dump(f)
|
|
else:
|
|
print(__doc__)
|