Read and patch UE3 packages and soundbanks
This commit is contained in:
142
ackit/lzo.py
Normal file
142
ackit/lzo.py
Normal file
@@ -0,0 +1,142 @@
|
||||
"""LZO1X, as used by the game's compressed UE3 packages (docs/formats/upk.md).
|
||||
|
||||
`decompress` is a pure-Python LZO1X decoder (same semantics as lzo1x_decompress_safe; the packages
|
||||
use 128 KiB blocks, so speed is fine for block-level access). `compress_stored` writes a valid LZO1X
|
||||
stream that holds the data as one literal run (no matches): bigger than real compression, but any
|
||||
LZO1X decoder (the game's included) reads it back exactly, and nothing else has to change.
|
||||
"""
|
||||
M2_MAX_OFFSET = 0x0800
|
||||
|
||||
|
||||
class LzoError(ValueError):
|
||||
pass
|
||||
|
||||
|
||||
def _copy_match(out, dist, n):
|
||||
start = len(out) - dist
|
||||
if start < 0:
|
||||
raise LzoError("match distance beyond output start")
|
||||
if dist >= n:
|
||||
out += out[start:start + n]
|
||||
else: # overlapping: repeat the last `dist` bytes
|
||||
pat = bytes(out[start:])
|
||||
out += (pat * (n // dist + 1))[:n]
|
||||
|
||||
|
||||
def decompress(src, out_len=None):
|
||||
"""LZO1X block -> bytes. `out_len` (if given) is checked."""
|
||||
src = bytes(src)
|
||||
n = len(src)
|
||||
out = bytearray()
|
||||
ip = 0
|
||||
|
||||
def ext(t, base):
|
||||
nonlocal ip
|
||||
while src[ip] == 0:
|
||||
t += 255
|
||||
ip += 1
|
||||
t += base + src[ip]
|
||||
ip += 1
|
||||
return t
|
||||
|
||||
try:
|
||||
state = "loop"
|
||||
if src[0] > 17:
|
||||
t = src[0] - 17
|
||||
ip = 1
|
||||
if t < 4:
|
||||
state = "match_next"
|
||||
else:
|
||||
out += src[ip:ip + t]
|
||||
ip += t
|
||||
state = "first_literal_run"
|
||||
while True:
|
||||
if state == "loop":
|
||||
t = src[ip]
|
||||
ip += 1
|
||||
if t >= 16:
|
||||
state = "match"
|
||||
continue
|
||||
if t == 0:
|
||||
t = ext(0, 15)
|
||||
out += src[ip:ip + t + 3]
|
||||
ip += t + 3
|
||||
state = "first_literal_run"
|
||||
continue
|
||||
if state == "first_literal_run":
|
||||
t = src[ip]
|
||||
ip += 1
|
||||
if t >= 16:
|
||||
state = "match"
|
||||
continue
|
||||
dist = 1 + M2_MAX_OFFSET + (t >> 2) + (src[ip] << 2)
|
||||
ip += 1
|
||||
_copy_match(out, dist, 3)
|
||||
state = "match_done"
|
||||
continue
|
||||
if state == "match_next":
|
||||
out += src[ip:ip + t]
|
||||
ip += t
|
||||
t = src[ip]
|
||||
ip += 1
|
||||
state = "match"
|
||||
continue
|
||||
if state == "match":
|
||||
if t >= 64: # M2
|
||||
dist = 1 + ((t >> 2) & 7) + (src[ip] << 3)
|
||||
ip += 1
|
||||
_copy_match(out, dist, (t >> 5) + 1)
|
||||
elif t >= 32: # M3
|
||||
t &= 31
|
||||
if t == 0:
|
||||
t = ext(0, 31)
|
||||
dist = 1 + ((src[ip] | (src[ip + 1] << 8)) >> 2)
|
||||
ip += 2
|
||||
_copy_match(out, dist, t + 2)
|
||||
elif t >= 16: # M4 (distance 0 = end of stream)
|
||||
hi = (t & 8) << 11
|
||||
t &= 7
|
||||
if t == 0:
|
||||
t = ext(0, 7)
|
||||
dist = hi + ((src[ip] | (src[ip + 1] << 8)) >> 2)
|
||||
ip += 2
|
||||
if dist == 0:
|
||||
break
|
||||
_copy_match(out, dist + 0x4000, t + 2)
|
||||
else: # M1 after a match: 2 bytes
|
||||
dist = 1 + (t >> 2) + (src[ip] << 2)
|
||||
ip += 1
|
||||
_copy_match(out, dist, 2)
|
||||
state = "match_done"
|
||||
continue
|
||||
# match_done: the low 2 bits of the instruction's last byte = trailing literals
|
||||
t = src[ip - 2] & 3
|
||||
if t == 0:
|
||||
state = "loop"
|
||||
else:
|
||||
state = "match_next"
|
||||
except IndexError:
|
||||
raise LzoError("truncated LZO stream") from None
|
||||
if ip != n:
|
||||
raise LzoError("%d bytes after the end of the LZO stream" % (n - ip))
|
||||
if out_len is not None and len(out) != out_len:
|
||||
raise LzoError("LZO block decoded to %d bytes, expected %d" % (len(out), out_len))
|
||||
return bytes(out)
|
||||
|
||||
|
||||
EOF = b"\x11\x00\x00"
|
||||
|
||||
|
||||
def compress_stored(data):
|
||||
"""bytes -> LZO1X stream made of one literal run (decodes back to `data`)."""
|
||||
data = bytes(data)
|
||||
n = len(data)
|
||||
if n == 0:
|
||||
return EOF
|
||||
if n <= 238:
|
||||
return bytes([n + 17]) + data + EOF
|
||||
# long literal run: 0x00, k zero bytes (255 each), last byte; length = 18 + 255*k + last
|
||||
r = n - 18
|
||||
k = (r - 1) // 255
|
||||
last = r - 255 * k
|
||||
return b"\x00" + b"\x00" * k + bytes([last]) + data + EOF
|
||||
Reference in New Issue
Block a user