Files
UnheardCity/tests/test_banks.py

170 lines
7.0 KiB
Python

"""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"}
neutral = synth.FOOT + synth.FOLEY + synth.GUI + (synth.DLG_MEDIA,)
assert set(bank) == {"%08X" % m for m in neutral + (synth.GRUNT_MEDIA["INT"],)} # INT catalog: no ITA copy
grunt = bank["%08X" % synth.GRUNT_MEDIA["INT"]]
assert grunt.language == "INT" and grunt.name == synth.GRUNT_EVENT and grunt.group == "Speech"
ita = Workspace(root, store_root=str(tmp_path / "s2"), archives=["BANK"]).catalog # no language: all
assert {s.language for s in ita.sounds if s.bank == synth.GRUNT_BANK} == {"INT", "ITA"}
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():
if s.language:
continue
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")