Add the REM interface generators (tools) and ignore dumps, logs and server files

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.
This commit is contained in:
2026-10-20 14:01:12 +02:00
parent 574536f419
commit 6eb785d57c
7 changed files with 598 additions and 1 deletions
+151
View File
@@ -0,0 +1,151 @@
"""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__)