Files
UnheardCity/tests/test_catalog.py

183 lines
9.5 KiB
Python

"""Catalog: classification rules, naming, character labels, search; the shipped CSVs."""
import collections
import csv
import os
import pytest
from ackit import banks, classify
from ackit.catalog import Catalog, read_csv, load_characters
from ackit.game import Game
from ackit.paths import catalog_dir
@pytest.mark.parametrize("name,group,category", [
("Speech_TAPE_JOKER_1_4", "Speech", "Interview tapes"),
("Speech_CINEMATIC_CH1_CATWOMAN_SAVED_9", "Speech", "Cinematics"),
("Speech_PRED_THUG1_FULL_Nervous_112", "Speech", "Predator chatter"),
("CHR_RadioScanner_12", "Speech", "Radio & TV"),
("AMB_Steel_B3", "Ambience", "Ambience"),
("Music_393BC53A", "Music", "Music"),
("6C151539_1", "Unknown", "Unknown"),
])
def test_classify(name, group, category):
assert classify.classify(name) == (group, category)
def test_pack_media_are_music():
assert classify.classify("003CB311", pack="Music") == ("Music", "Music")
assert classify.classify("X", pack="Music_PDLC")[1] == "Music (DLC)"
def test_infer_characters():
assert classify.infer_characters("Speech_TAPE_MADHATTER_5_10") == ["Hugo Strange", "Mad Hatter"]
assert "TYGER guard" in classify.infer_characters("Speech_PRED_TYGER2_Generic_7")
assert classify.infer_characters("Speech_COMBAT_BARKS_BANE_Hit_batman_3")[:2] == ["Bane", "Batman"]
def test_synthetic_catalog_names_and_search(synth_game):
root, _ = synth_game
cat = Catalog(Game(root), ["INT", "SFX"])
names = {s.name for s in cat.sounds}
assert {"CHR_RadioScanner_1", "Speech_Synth_Radio_02", "CHR_RadioScanner_3", "Music_393BC53A"} <= names
s = cat.resolve("CHR_RadioScanner#2")
assert s.target.endswith(":1") and cat.resolve("speech_synth_radio_02.wav") is s
assert s.line and s.name == "Speech_Synth_Radio_02" # the game's line: named after its event
assert cat.resolve(s.key) is s
assert [x.name for x in cat.search("radio scanner 3")] == ["CHR_RadioScanner_3"]
with pytest.raises(LookupError) as e:
cat.resolve("CHR_RadioScaner_2")
assert "did you mean" in str(e.value)
def test_characters_csv_covers_user_lists():
chars = load_characters(lang="ITA")
assert len(chars) > 3000 and len(load_characters()) > 2500 # most labels apply in every language
# the lists name ITA sounds by position (wemscan): NAME_i = variation i of the ITA WAD entry (alias NAME#i)
src = os.path.join(catalog_dir(), "source", "ita", "Joker.txt")
first = open(src).readline().strip()[:-4]
base, i = first.rsplit("_", 1)
ita = read_csv(os.path.join(catalog_dir(), "ITA.csv"))
s = next(s for s in ita if first.lower() in {n.lower() for n in s.names()} or
"%s#%s" % (base, i) in s.aliases)
assert "Joker" in s.character
# every labelled ITA sound keeps its label (takes of one line, NAME#k, share it), and most labels reach INT
assert sum(1 for x in ita if x.character) >= 3366
assert sum(1 for x in read_csv(os.path.join(catalog_dir(), "INT.csv")) if x.character) > 2900
def test_line_names_agree_across_languages_and_subtitles():
"""Speech sounds carry the game's line name: the same names in every language, and a linked subtitle's
sound has the subtitle's line name (positions inside WAD entries differ per language: findings 20)."""
from ackit import subtitles
ita = {s.name for s in read_csv(os.path.join(catalog_dir(), "ITA.csv")) if s.group == "Speech"}
intl = {s.name for s in read_csv(os.path.join(catalog_dir(), "INT.csv")) if s.group == "Speech"}
assert len(ita & intl) > 0.97 * len(ita)
snd = {s.key: s for s in read_csv(os.path.join(catalog_dir(), "ITA.csv"))}
rows = [r for r in subtitles.read_texts(os.path.join(catalog_dir(), "SUB_ITA.csv"), "ITA") if r.sound in snd]
same = sum(1 for r in rows if snd[r.sound].name.split("#")[0] == r.speech)
assert len(rows) > 8000 and same > 0.99 * len(rows)
hdr = next(csv.reader(open(os.path.join(catalog_dir(), "characters.csv"), encoding="utf-8")))
assert hdr == ["name", "character", "language"]
def test_shipped_csvs_are_consistent():
from ackit import subtitles
for f in os.listdir(catalog_dir()):
if f.startswith("SUB_") and f.endswith(".csv"):
lines = subtitles.read_texts(os.path.join(catalog_dir(), f), f[4:7])
assert len({(s.speech, s.line) for s in lines}) == len(lines), f
assert all(len(s.text) <= s.room for s in lines), f
continue
if f.endswith(".csv") and f != "characters.csv":
rows = read_csv(os.path.join(catalog_dir(), f))
keys = {r.key for r in rows}
assert len(keys) == len(rows), f
assert all(r.group in classify.GROUPS for r in rows)
assert len({(r.language, r.name.lower()) for r in rows}) == len(rows), f # per language
# no checksum only where the copies of a BANK sound differ (1C898731)
assert sum(len(r.sha1) != 16 for r in rows) <= (1 if f == "BANK.csv" else 0), f
hdr = next(csv.reader(open(os.path.join(catalog_dir(), "ITA.csv"), encoding="utf-8")))
assert hdr[:3] == ["archive", "target", "name"]
locs = banks.read_locations(os.path.join(catalog_dir(), banks.LOCATIONS))
bank = {r.target for r in read_csv(os.path.join(catalog_dir(), "BANK.csv"))}
assert {"%08X" % m for m, ls in locs.items() if ls[0].kind == "m"} == bank
assert all(l.sound for ls in locs.values() for l in ls if l.kind == "p")
def test_shipped_catalog_cross_references():
"""Every reference between the shipped files points to something that exists, and every line has one
recording and one name per language (copies aside)."""
from ackit import subtitles
voices = ["INT", "FRA", "ITA", "DEU", "ESN"]
rows = {a: read_csv(os.path.join(catalog_dir(), a + ".csv")) for a in voices + ["SFX", "BANK"]}
keys = {s.key: s for r in rows.values() for s in r}
bank = rows["BANK"]
# subtitles -> a voice recording of that language (INT voices for the others)
for f in os.listdir(catalog_dir()):
if f.startswith("SUB_") and f.endswith(".csv"):
lang = f[4:7]
voice = lang if lang in voices else "INT"
for s in subtitles.read_texts(os.path.join(catalog_dir(), f), lang):
for k in filter(None, s.sound.split(";")):
assert k in keys, (f, s.speech, k)
snd = keys[k]
assert snd.archive == voice or (snd.archive == "BANK" and snd.in_language(voice)), (f, k)
# heads of WAD recordings -> a WAD recording, never larger than it; no BANK sound is also a head
locs = banks.read_locations(os.path.join(catalog_dir(), banks.LOCATIONS))
for m, ls in locs.items():
for l in ls:
if l.kind == "p":
assert l.sound in keys and keys[l.sound].archive in voices and l.size <= keys[l.sound].size, l
assert "BANK:%08X" % m not in keys
# speaker labels -> line names that exist in every voice language
names = {L: {s.name for s in rows[L]} for L in voices}
with open(os.path.join(catalog_dir(), "characters.csv"), encoding="utf-8") as f:
for r in csv.DictReader(f):
assert all(r["name"] in names[L] for L in ([r["language"]] if r["language"] else voices)), r
# one recording per language per line (copies: a second place of the same recording, same line)
for L in voices:
per = collections.defaultdict(list)
for s in rows[L] + [b for b in bank if b.line and b.in_language(L)]:
if s.line:
per[s.line].append(s)
for lid, ss in per.items():
assert len({s.sha1 for s in ss}) == 1, (L, lid, [s.key for s in ss]) # copies are identical
assert len([s for s in ss if s.archive == "BANK"]) <= 1, (L, lid)
# the same line has the same name in every language (the second place of a copy keeps its position name)
common = {}
for L in voices:
per = collections.defaultdict(set)
for s in rows[L]:
if s.line:
per[s.line].add(s.name)
for lid, ns in per.items():
common[lid] = common[lid] & ns if lid in common else ns
assert all(common.values()), [k for k, v in common.items() if not v][:5]
@pytest.mark.game
def test_real_game_uses_shipped_catalog(real_game_dir):
g = Game(real_game_dir)
cat = Catalog(g, ["ITA", "SFX", "BANK"])
bank = read_csv(os.path.join(catalog_dir(), "BANK.csv"))
assert len(cat.sounds) == 10816 + 651 + sum(1 for s in bank if s.in_language("ITA"))
assert all(s.in_language("ITA") for s in cat.sounds if s.archive == "BANK")
assert all(s.line for s in cat.sounds if s.archive == "ITA" and s.group == "Speech") # every voice line
s = cat.resolve("Speech_TAPE_JOKER_1#4") # position 4 in the ITA entry (the user's list name)
assert s.category == "Interview tapes" and s.size == 44985 and s.name.startswith("Speech_TAPE_JOKER_1_")
# bank copies of WAD recordings: 396 prefetch heads + 49 recordings also stored whole in banks
assert len(cat.heads) == 445 and sum(len(v) for v in cat.heads.values()) == 2185
b = cat.resolve("Play_BatmanDeviceImpact_Wood_1")
assert b.archive == "BANK" and b.group == "SFX"
@pytest.mark.game
def test_every_name_resolves_to_its_sound(real_game_dir):
"""A line named after its INT position (CHR_RadioScanner#1) can equal another recording's position alias
in another language: the name wins, in every language."""
g = Game(real_game_dir)
for lang in g.languages:
cat = Catalog(g, [lang, "SFX", "BANK"])
assert all(cat.resolve(s.name) is s for s in cat.sounds), lang