List and edit in-memory sounds in soundbanks
This commit is contained in:
162
tests/test_banks.py
Normal file
162
tests/test_banks.py
Normal file
@@ -0,0 +1,162 @@
|
||||
"""In-memory sounds (archive BANK) and prefetch heads: catalog scan, apply/revert through Workspace."""
|
||||
import hashlib
|
||||
import os
|
||||
|
||||
import pytest
|
||||
|
||||
import synth
|
||||
from ackit import banks
|
||||
from ackit.game import Game
|
||||
from ackit.inject import SizeLimitError
|
||||
from ackit.upk import Package
|
||||
from ackit.workspace import Workspace, ConflictError, MODIFIED, ORIGINAL
|
||||
|
||||
|
||||
def _sha(p):
|
||||
return hashlib.sha1(open(p, "rb").read()).hexdigest()
|
||||
|
||||
|
||||
def _ws(root, tmp_path):
|
||||
return Workspace(root, store_root=str(tmp_path / "store"), archives=["INT", "SFX", "BANK"])
|
||||
|
||||
|
||||
def _cooked(root):
|
||||
return Game(root).cooked
|
||||
|
||||
|
||||
def _copy(ws, l):
|
||||
return banks.open_file(ws.bank_path(l.file)).read(l.offset, l.size)
|
||||
|
||||
|
||||
def test_bank_catalog(synth_game, tmp_path):
|
||||
root, originals = synth_game
|
||||
ws = _ws(root, tmp_path)
|
||||
bank = {s.target: s for s in ws.catalog.sounds if s.archive == "BANK"}
|
||||
assert set(bank) == {"%08X" % m for m in synth.FOOT + synth.FOLEY + synth.GUI}
|
||||
foot = bank["%08X" % synth.FOOT[0]]
|
||||
assert foot.bank == "CHR_SynthFootsteps_Media" and foot.name == "CHR_SynthFootsteps_Media_1"
|
||||
assert (foot.group, foot.category) == ("SFX", "Footsteps")
|
||||
assert bank["%08X" % synth.GUI[0]].category == "Interface"
|
||||
foley = bank["%08X" % synth.FOLEY[1]]
|
||||
assert foley.rate == 32000 and foley.size == len(originals[foley.target])
|
||||
assert sorted(l.file for l in ws.locations(foley)) == ["Synth_A.upk", "Synth_B.upk"]
|
||||
for s in bank.values():
|
||||
assert ws.current_bytes(s) == originals[s.target]
|
||||
assert all(_copy(ws, l) == originals[s.target] for l in ws.locations(s))
|
||||
# the prefetch head belongs to INT variation 1 of the speech entry, not to the BANK archive
|
||||
line = ws.catalog.by_key["INT:%08X:1" % synth.SPEECH_ENTRY]
|
||||
heads = ws.locations(line)
|
||||
assert [(l.file, l.size, l.kind) for l in heads] == [("Synth_A.upk", synth.HEAD, "p")]
|
||||
assert _copy(ws, heads[0]) == originals["%08X:1" % synth.SPEECH_ENTRY][:synth.HEAD]
|
||||
assert ws.locations(ws.catalog.by_key["INT:%08X:0" % synth.SPEECH_ENTRY]) == []
|
||||
|
||||
|
||||
def test_bank_apply_every_copy_and_revert_byte_identical(synth_game, tmp_path):
|
||||
root, originals = synth_game
|
||||
ws = _ws(root, tmp_path)
|
||||
cooked = _cooked(root)
|
||||
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)
|
||||
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))
|
||||
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)
|
||||
assert Package(files[1]).size == Package(ws.store.full_copy(ws.game.rel(files[1]))).size # layout unchanged
|
||||
ws.revert([s])
|
||||
assert ws.state(s) == ORIGINAL and {f: _sha(f) for f in files} == before
|
||||
|
||||
|
||||
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), {})])
|
||||
assert os.path.getsize(path) == size and _sha(path) != before
|
||||
ws.revert_all()
|
||||
assert _sha(path) == before
|
||||
|
||||
|
||||
def test_partial_revert_keeps_other_changes(synth_game, tmp_path):
|
||||
root, originals = synth_game
|
||||
ws = _ws(root, tmp_path)
|
||||
path = os.path.join(_cooked(root), "Synth_B.upk")
|
||||
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)
|
||||
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
|
||||
ws.revert([b])
|
||||
assert _sha(path) == before
|
||||
|
||||
|
||||
def test_speech_line_with_prefetch_head(synth_game, tmp_path):
|
||||
root, originals = synth_game
|
||||
ws = _ws(root, tmp_path)
|
||||
line = ws.catalog.by_key["INT:%08X:1" % synth.SPEECH_ENTRY]
|
||||
files = ws.files(line)
|
||||
assert [os.path.basename(f) for f in files] == ["28CCF006_LOC_INT.wad", "Synth_A.upk"]
|
||||
before = {f: _sha(f) for f in files}
|
||||
new = synth.wem(0.3, 640)
|
||||
ws.apply([(line, new, {})])
|
||||
head = ws.locations(line)[0]
|
||||
assert _copy(ws, head) == (new + bytes(synth.HEAD))[:synth.HEAD]
|
||||
assert ws.current_bytes(line).startswith(new)
|
||||
ws.revert([line])
|
||||
assert {f: _sha(f) for f in files} == before
|
||||
|
||||
|
||||
def test_bank_size_limit(synth_game, tmp_path):
|
||||
root, _ = synth_game
|
||||
ws = _ws(root, tmp_path)
|
||||
s = ws.catalog.by_key["BANK:%08X" % synth.FOOT[1]]
|
||||
with pytest.raises(SizeLimitError):
|
||||
ws.apply([(s, synth.wem(3.0, 440), {})])
|
||||
assert ws.store.entries == {}
|
||||
|
||||
|
||||
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), {})])
|
||||
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.revert([s])
|
||||
assert _sha(path) == _sha(ws.store.full_copy(ws.game.rel(path)))
|
||||
|
||||
|
||||
def test_event_names():
|
||||
sc = banks.Scan()
|
||||
sc.names = {"Play_Synth_Punch", "Stop_Synth_Punch", "Unrelated"}
|
||||
sc.bank_names = {7: "CHR_SynthCombat"}
|
||||
for m in (1, 2, 3):
|
||||
sc.media[m] = {"size": 10, "sha1": {"x"}, "head": b"", "banks": {7}, "kinds": {"m"}}
|
||||
# event -> action -> random container (children 0x50, 0x51) -> sounds -> media 1, 2; media 3 unreferenced
|
||||
from ackit.bnk import fnv1
|
||||
import struct
|
||||
sc.objects = {0x50: 2, 0x51: 2, 0x60: 5, 0x70: 3, fnv1("Play_Synth_Punch"): 4}
|
||||
sc.containers = {0x60: struct.pack("<I", 0x60) + bytes(9) + struct.pack("<3I", 2, 0x50, 0x51) + bytes(5)}
|
||||
sc.actions = {0x70: 0x60}
|
||||
sc.events = {fnv1("Play_Synth_Punch"): [0x70]}
|
||||
sc.sound_media = {0x50: {1}, 0x51: {2}}
|
||||
names = banks.media_names(sc)
|
||||
assert names[1] == ("Play_Synth_Punch_1", "CHR_SynthCombat")
|
||||
assert names[2] == ("Play_Synth_Punch_2", "CHR_SynthCombat")
|
||||
assert names[3] == ("CHR_SynthCombat_1", "CHR_SynthCombat")
|
||||
Reference in New Issue
Block a user