"""Subtitles (dialogue objects in packages) and menu text (localization files): parse, apply, revert.""" import hashlib import os import pytest import synth from ackit import loctext, subtitles from ackit.game import Game from ackit.texts import MODIFIED, ORIGINAL, TextConflictError from ackit.workspace import Workspace SUB_KEY = "SUB:INT:%s:0" % synth.SPEECH_EVENT 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 _files(root): d = os.path.join(root, "BmGame") return {os.path.join(a, f): _sha(os.path.join(a, f)) for a, _, fs in os.walk(d) for f in fs} # --- formats ------------------------------------------------------------------------------------ def test_fstring_encoding(): b = subtitles.encode_fstring("Hi", 10, "a") assert b == b"\x06\x00\x00\x00Hi\0\0\0\0" and subtitles.read_fstring(b, 0)[0] == "Hi" w = subtitles.encode_fstring("Привет", 4 + 2 * 8, "w") assert subtitles.read_fstring(w, 0) == ("Привет", 20, "w") with pytest.raises(subtitles.SubtitleError): subtitles.encode_fstring("too long", 10, "a") with pytest.raises(subtitles.SubtitleError): subtitles.encode_fstring("→", 10, "a") # not in cp1252 def test_menu_parse_and_write(): fname, text = synth.MENU["INT"] raw = b"\xff\xfe" + text.encode("utf-16-le") es = {e.key: e for e in loctext.parse(raw, "INT", fname)} assert es["INT:FRONTEND.int:Menu:Start"].value == "Press any key" assert es["INT:FRONTEND.int:Menu:Quit"].quoted and es["INT:FRONTEND.int:Tips:Tip#2"].value == "Glide over the city" assert loctext.write(raw, "INT", fname, {}) == raw new = loctext.write(raw, "INT", fname, {"INT:FRONTEND.int:Menu:Quit": "Leave", "INT:FRONTEND.int:Tips:Tip#2": "Fly"}) assert new.decode("utf-16") == text.replace('"Quit game"', '"Leave"').replace("Glide over the city", "Fly") with pytest.raises(loctext.TextError): loctext.write(raw, "INT", fname, {"INT:FRONTEND.int:Menu:Start": "two\nlines"}) # --- subtitles through the workspace ------------------------------------------------------------ def test_subtitle_catalog_and_link(synth_game, tmp_path): root, _ = synth_game ws = _ws(root, tmp_path) lines = ws.texts.subtitles("INT") assert [(s.speech, s.line, s.text) for s in lines] == [(synth.GRUNT_EVENT, 0, "Hey you!"), (synth.SPEECH_EVENT, 0, "Scanner line two."), (synth.SPEECH_EVENT, 1, "Second part.")] # a line kept in memory: each language's subtitle links to that language's BANK sound grunt = ws.catalog.by_key["BANK:%08X" % synth.GRUNT_MEDIA["INT"]] assert [s.text for s in ws.texts.for_sound(grunt)] == ["Hey you!"] ita = {s.speech: s.sound for s in ws.texts.subtitles("ITA")} assert ita[synth.GRUNT_EVENT] == "BANK:%08X" % synth.GRUNT_MEDIA["ITA"] line = ws.catalog.by_key["INT:%08X:1" % synth.SPEECH_ENTRY] assert [s.text for s in ws.texts.for_sound(line)] == ["Scanner line two.", "Second part."] assert ita[synth.SPEECH_EVENT] == line.key # no ITA speech WAD here: INT line assert ws.texts.subtitles("RUS")[0].enc == "w" assert sorted(os.path.basename(p) for p in ws.texts.files(SUB_KEY)) == ["Synth_Dialogue.upk", "Synth_Dialogue2.upk"] assert ws.texts.limit(SUB_KEY) == len("Scanner line two.") def test_subtitle_apply_revert_every_copy(synth_game, tmp_path): root, _ = synth_game ws = _ws(root, tmp_path) before = _files(root) ws.texts.apply({SUB_KEY: "Line two.", "SUB:RUS:%s:0" % synth.SPEECH_EVENT: "Две"}) assert ws.texts.current(SUB_KEY) == "Line two." and ws.texts.state(SUB_KEY) == MODIFIED for p in ws.texts.files(SUB_KEY): # every copy ws2 = _ws(root, tmp_path / "other") assert ws2.texts.current(SUB_KEY) == "Line two." with pytest.raises(subtitles.SubtitleError): ws.texts.apply({SUB_KEY: "This line is far too long for the room"}) ws.texts.revert([SUB_KEY]) assert ws.texts.current(SUB_KEY) == "Scanner line two." and ws.texts.current("SUB:RUS:%s:0" % synth.SPEECH_EVENT) == "Две" ws.texts.revert_all() assert _files(root) == before and ws.texts.state(SUB_KEY) == ORIGINAL def test_subtitle_and_sound_in_one_package(synth_game, tmp_path): root, _ = synth_game ws = _ws(root, tmp_path) before = _files(root) s = ws.catalog.by_key["BANK:%08X" % synth.DLG_MEDIA] 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]) assert ws.texts.current(SUB_KEY) == "Both." and ws.state(s) == "original" ws.texts.revert_all() assert _files(root) == before # --- menu text through the workspace ------------------------------------------------------------ def test_menu_apply_revert(synth_game, tmp_path): root, _ = synth_game ws = _ws(root, tmp_path) before = _files(root) assert "INT" in ws.texts.menu_languages() keys = {e.key: e for e in ws.texts.menu("INT")} assert "INT:Version.int:ID:Unreal" not in keys k = "MENU:INT:FRONTEND.int:Menu:Start" ws.texts.apply({k: "Press Enter"}) assert ws.texts.current(k) == "Press Enter" and ws.texts.state(k) == MODIFIED assert {e.key: e.value for e in ws.texts.menu("INT")}["INT:FRONTEND.int:Menu:Start"] == "Press Enter" ws.texts.revert([k]) assert _files(root) == before def test_menu_changed_underneath(synth_game, tmp_path): root, _ = synth_game ws = _ws(root, tmp_path) k = "MENU:INT:FRONTEND.int:Tips:Tip#2" ws.texts.apply({k: "Glide"}) path = ws.texts.files(k)[0] raw = open(path, "rb").read() open(path, "wb").write(loctext.write(raw, "INT", "FRONTEND.int", {"INT:FRONTEND.int:Menu:Start": "Other tool"})) with pytest.raises(TextConflictError): ws.texts.apply({k: "Soar"}) ws.texts.apply({k: "Soar"}, force=True) assert ws.texts.current(k) == "Soar" ws.texts.revert_all() assert open(path, "rb").read() == open(ws.store.full_copy(ws.game.rel(path)), "rb").read() # --- real game (read-only) ---------------------------------------------------------------------- def test_real_subtitles_and_menu(real_game_dir, tmp_path): ws = Workspace(Game(real_game_dir), store_root=str(tmp_path / "store"), archives=["ITA"]) lines = ws.texts.subtitles("ITA") assert len(lines) > 9000 s = next(x for x in lines if x.sound) assert ws.texts.current(subtitles.key("ITA", s.speech, s.line)) == s.text menu = ws.texts.menu("ITA") assert len(menu) > 7000 and all(e.lang == "ITA" for e in menu)