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

@@ -25,6 +25,16 @@ def wem(seconds, freq, rate=24000, ch=1):
return encoder.encode(x, sr, rate, ch)[0]
def fitting_wem(ws, s, seconds, freq):
"""A tone WEM that `ws` accepts for `s` (Workspace.fits: in-memory slots are filled exactly, which needs
an even number of spare bytes): the tone is shortened a little until the size allows it."""
for k in range(40):
w = wem(seconds * (1 - 0.01 * k), freq)
if ws.fits(s, w):
return w
raise AssertionError("no fitting WEM for %s" % s.key)
def _a(x, n):
return (x + n - 1) // n * n

View File

@@ -9,6 +9,7 @@ from ackit import banks
from ackit.game import Game
from ackit.inject import SizeLimitError
from ackit.upk import Package
from ackit.wem import fill, unfill
from ackit.workspace import Workspace, ConflictError, MODIFIED, ORIGINAL
@@ -65,12 +66,13 @@ def test_bank_apply_every_copy_and_revert_byte_identical(synth_game, tmp_path):
files = [os.path.join(cooked, f) for f in ("Synth_A.upk", "Synth_B.upk")]
before = {f: _sha(f) for f in files}
s = ws.catalog.by_key["BANK:%08X" % synth.FOLEY[0]]
new = synth.wem(0.2, 1500)
new = synth.fitting_wem(ws, s, 0.2, 1500)
assert len(new) < ws.limit(s)
ws.apply([(s, new, {"source": "test"})])
assert ws.state(s) == MODIFIED
for l in ws.locations(s):
assert _copy(ws, l) == new + bytes(l.size - len(new))
assert ws.state(s) == MODIFIED and ws.current_bytes(s) == new
for l in ws.locations(s): # every copy filled exactly: no zero padding after the WEM
assert _copy(ws, l) == fill(new, l.size) and len(_copy(ws, l)) == l.size
assert unfill(_copy(ws, l)) == new
other = ws.catalog.by_key["BANK:%08X" % synth.FOLEY[1]]
assert all(_copy(ws, l) == originals[other.target] for l in ws.locations(other))
assert all(_sha(f) != before[f] for f in files)
@@ -79,13 +81,49 @@ def test_bank_apply_every_copy_and_revert_byte_identical(synth_game, tmp_path):
assert ws.state(s) == ORIGINAL and {f: _sha(f) for f in files} == before
def test_fill_slot_exactly():
"""The game plays an in-memory WEM followed by zero padding as silence: a smaller replacement is grown to
its slot with a label list chunk (wem.fill), which the game's own WEMs use too."""
from ackit import audio
from ackit.wem import FILL_MIN, Wem, fillable
w = synth.wem(0.2, 1500)
for size in (len(w), len(w) + FILL_MIN, len(w) + 1000):
f = fill(w, size)
assert len(f) == size and 8 + int.from_bytes(f[4:8], "little") == size # RIFF covers the slot
tags = [b"fmt ", b"data"] if size == len(w) else [b"fmt ", b"LIST", b"data"]
assert unfill(f) == w and [c.tag for c in Wem.parse(f).chunks] == tags
assert (audio.decode_wem(f)[0] == audio.decode_wem(w)[0]).all()
assert not fillable(len(w), len(w) + 3) and not fillable(len(w), len(w) + FILL_MIN - 2)
with pytest.raises(ValueError):
fill(w, len(w) + 1)
f = fill(w, len(w) + 40)
a = f.index(b"adtllabl")
game_like = f[:a + 12] + (1).to_bytes(4, "little") + b"END\0" + f[a + 20:] # a real marker label: kept
assert len(game_like) == len(f) and unfill(game_like) == game_like
def test_bank_apply_refuses_unfillable_and_prepare_fits(synth_game, tmp_path):
from ackit.game import GameError
root, _ = synth_game
ws = _ws(root, tmp_path)
s = ws.catalog.by_key["BANK:%08X" % synth.FOOT[1]]
bad = next(w for w in (synth.wem(0.1 * (1 - 0.01 * k), 2000) for k in range(40)) if not ws.fits(s, w)
and len(w) <= ws.limit(s))
with pytest.raises(GameError):
ws.apply([(s, bad, {})])
assert ws.store.entries == {}
x, sr = synth.tone(0.1, 24000, 2000, 1)
wem, _ = ws.prepare(s, x, sr)
assert ws.fits(s, wem)
def test_loose_bank_in_place(synth_game, tmp_path):
root, originals = synth_game
ws = _ws(root, tmp_path)
s = ws.catalog.by_key["BANK:%08X" % synth.FOOT[1]]
path = ws.files(s)[0]
before, size = _sha(path), os.path.getsize(path)
ws.apply([(s, synth.wem(0.1, 2000), {})])
ws.apply([(s, synth.fitting_wem(ws, s, 0.1, 2000), {})])
assert os.path.getsize(path) == size and _sha(path) != before
ws.revert_all()
assert _sha(path) == before
@@ -98,11 +136,11 @@ def test_partial_revert_keeps_other_changes(synth_game, tmp_path):
before = _sha(path)
a = ws.catalog.by_key["BANK:%08X" % synth.FOLEY[0]]
b = ws.catalog.by_key["BANK:%08X" % synth.GUI[0]]
na, nb = synth.wem(0.2, 1500), synth.wem(0.2, 1700)
na, nb = synth.fitting_wem(ws, a, 0.2, 1500), synth.fitting_wem(ws, b, 0.2, 1700)
ws.apply([(a, na, {}), (b, nb, {})])
ws.revert([a])
assert ws.current_bytes(a) == originals[a.target]
assert ws.current_bytes(b).startswith(nb) and ws.state(b) == MODIFIED
assert ws.current_bytes(b) == nb and ws.state(b) == MODIFIED
ws.revert([b])
assert _sha(path) == before
@@ -136,15 +174,15 @@ def test_bank_file_changed_underneath(synth_game, tmp_path):
root, originals = synth_game
ws = _ws(root, tmp_path)
s = ws.catalog.by_key["BANK:%08X" % synth.GUI[0]]
ws.apply([(s, synth.wem(0.2, 1500), {})])
ws.apply([(s, synth.fitting_wem(ws, s, 0.2, 1500), {})])
path = ws.files(s)[0]
# another tool rewrites the package (different bytes elsewhere in it)
pk = Package(path)
other = pk.patched([(pk.header_size + 10, b"OTHER-TOOL")])
open(path, "wb").write(other)
with pytest.raises(ConflictError):
ws.apply([(s, synth.wem(0.2, 1600), {})])
ws.apply([(s, synth.wem(0.2, 1600), {})], force=True)
ws.apply([(s, synth.fitting_wem(ws, s, 0.2, 1600), {})])
ws.apply([(s, synth.fitting_wem(ws, s, 0.2, 1600), {})], force=True)
ws.revert([s])
assert _sha(path) == _sha(ws.store.full_copy(ws.game.rel(path)))

View File

@@ -9,6 +9,7 @@ from ackit import banks
from ackit.bnk import fnv1
from ackit.catalog import build_lines
from ackit.game import Game
from ackit.wem import fill
from ackit.workspace import ORIGINAL, MODIFIED, Workspace
import synth
@@ -138,8 +139,9 @@ def test_whole_recording_in_a_bank_is_written_with_its_line(tmp_path):
assert l.kind == banks.PREFETCH_HEAD and l.size == len(a["INT"]) and l.file == "Lines.upk"
pkg = os.path.join(ws.game.cooked, "Lines.upk")
before = hashlib.sha1(open(pkg, "rb").read()).hexdigest()
new = synth.wem(0.3, 900)
new = synth.fitting_wem(ws, s, 0.3, 900)
ws.apply([(s, new, {})])
assert banks.open_file(pkg).read(l.offset, l.size) == new + bytes(l.size - len(new))
assert banks.open_file(pkg).read(l.offset, l.size) == fill(new, l.size) # filled exactly, not padded
assert ws.current_bytes(s) == new
ws.revert([s])
assert hashlib.sha1(open(pkg, "rb").read()).hexdigest() == before

View File

@@ -94,7 +94,7 @@ def test_subtitle_and_sound_in_one_package(synth_game, tmp_path):
ws = _ws(root, tmp_path)
before = _files(root)
s = ws.catalog.by_key["BANK:%08X" % synth.DLG_MEDIA]
ws.apply([(s, synth.wem(0.1, 1900), {})])
ws.apply([(s, synth.fitting_wem(ws, s, 0.1, 1900), {})])
ws.texts.apply({SUB_KEY: "Both."})
assert ws.texts.current(SUB_KEY) == "Both." and ws.state(s) == "modified"
ws.revert([s])