Version 3.2.2: in-memory replacements fill their slots exactly (padded ones played as silence)

This commit is contained in:
2026-10-11 00:10:44 +02:00
parent cac52e7597
commit b6b96307ca
16 changed files with 223 additions and 52 deletions

View File

@@ -101,6 +101,60 @@ class Wem:
return b"RIFF" + struct.pack("<I", len(body)) + body
# ---- filling an in-memory slot exactly ----
# The game ships no in-memory WEM with bytes after its RIFF inside its slot, and plays a replacement that is
# followed by zero padding there as silence (findings 24). A smaller replacement is therefore grown to the
# slot's exact size with a label list chunk before `data`, the form the game's own WEMs use for markers:
# LIST{'adtl', labl{u32 cue id 0, zero text}}. Even sizes only (the game's chunks before `data` are even).
FILL_MIN = 8 + 4 + 8 + 4 + 2
def fillable(length, size):
"""True if a WEM of `length` bytes can fill a slot of `size` bytes exactly."""
pad = size - length
return pad == 0 or (pad >= FILL_MIN and pad % 2 == 0)
def fill(wem: bytes, size: int) -> bytes:
"""`wem` grown to exactly `size` bytes (see above). ValueError if `fillable` is False."""
if not fillable(len(wem), size):
raise ValueError("a %d-byte WEM cannot fill a %d-byte slot exactly" % (len(wem), size))
pad = size - len(wem)
if not pad:
return wem
i = _data_chunk(wem)
text = pad - 8 - 4 - 8 - 4
chunk = (b"LIST" + struct.pack("<I", pad - 8) + b"adtl" + b"labl" + struct.pack("<II", text + 4, 0)
+ bytes(text))
out = wem[:i] + chunk + wem[i:]
return out[:4] + struct.pack("<I", len(out) - 8) + out[8:]
def unfill(wem: bytes) -> bytes:
"""The WEM without a filler chunk added by `fill` (a game WEM is returned unchanged)."""
i = _data_chunk(wem)
if i is None:
return wem
for j in range(12, i, 2):
if wem[j:j + 4] == b"LIST" and wem[j + 8:j + 16] == b"adtllabl":
n = 8 + struct.unpack_from("<I", wem, j + 4)[0]
if j + n == i and struct.unpack_from("<I", wem, j + 20)[0] == 0 and not wem[j + 24:i].strip(b"\0"):
out = wem[:j] + wem[i:]
return out[:4] + struct.pack("<I", len(out) - 8) + out[8:]
return wem
def _data_chunk(wem: bytes):
"""Offset of the `data` chunk header (walking the chunks), or None."""
pos = 12
while pos + 8 <= len(wem):
tag, ln = struct.unpack_from("<4sI", wem, pos)
if tag == b"data":
return pos
pos += 8 + ln
return None
SUB_ALIGN = 0x800
_NONZERO = re.compile(rb"[^\x00]")