"""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