Version 3.1.0: voice lines linked across all languages from the game data
This commit is contained in:
@@ -4,13 +4,15 @@
|
||||
|
||||
Outputs
|
||||
data/catalog/characters.csv name -> verified speaker, from data/catalog/source/<lang>/*.txt
|
||||
(speech sounds carry the game's line identity and one name in every language: ackit/lines.py)
|
||||
data/catalog/<ARCHIVE>.csv every sound of one archive (schema: data/catalog/README.md)
|
||||
data/catalog/BANK.csv in-memory sounds (soundbanks in SFX/*.bnk and the .upk packages)
|
||||
data/catalog/BANK_locations.csv.gz every stored copy of them + the prefetch heads of speech lines
|
||||
data/catalog/SUB_<LANG>.csv subtitles of the speech lines (one file per language)
|
||||
data/catalog/SUB_locations.csv.gz every stored copy of each subtitle string
|
||||
data/catalog/index.json fingerprints the CSVs were built from + counts
|
||||
Run it only on clean game files (`ackit status` must say nothing is modified).
|
||||
Run it only on clean game files (`ackit status` must say nothing is modified). It stops before writing
|
||||
anything if a sound differs from the current catalog (a modified game); after a game update use --accept-changes.
|
||||
"""
|
||||
import argparse
|
||||
import collections
|
||||
@@ -23,7 +25,7 @@ import time
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
from ackit import banks, settings, subtitles # noqa: E402
|
||||
from ackit.catalog import (apply_line_names, character_of, hash_names, scan_archive, scan_text, # noqa: E402
|
||||
from ackit.catalog import (apply_lines, character_of, hash_names, read_csv, scan_archive, scan_text, # noqa: E402
|
||||
write_csv)
|
||||
from ackit.classify import USER_LABELS # noqa: E402
|
||||
from ackit.game import Game, default_game_dir, wad_fingerprint # noqa: E402
|
||||
@@ -51,17 +53,28 @@ def read_user_lists(src_dir):
|
||||
return out
|
||||
|
||||
|
||||
def build_characters(sounds, src_root):
|
||||
"""Map the user's wemscan-style names onto catalog names (name or alias)."""
|
||||
by_alias = collections.defaultdict(list)
|
||||
for s in sounds:
|
||||
for a in s.names():
|
||||
by_alias[a.lower()].append(s)
|
||||
rows, unmatched, ambiguous = {}, [], []
|
||||
for lang in sorted(os.listdir(src_root)):
|
||||
d = os.path.join(src_root, lang)
|
||||
def build_characters(game, src_root, final):
|
||||
"""Map the curated lists onto the catalog. Each list folder (source/<lang>/) names sounds of THAT language's
|
||||
WAD by position (wemscan names: NAME = single entry, NAME_i = i-th WEM of the entry). Positions differ
|
||||
between languages, so a listed recording is found by its position in that language and the label goes to
|
||||
its line (`final`: {key: Sound} with line identities and names), i.e. to the same recording in every
|
||||
language; a recording without a line keeps the label in that language only.
|
||||
-> ({(name, language or ''): set(characters)}, unmatched names, ambiguous names, skipped folders)"""
|
||||
rows = collections.defaultdict(set)
|
||||
unmatched, ambiguous, skipped = [], [], []
|
||||
for folder in sorted(os.listdir(src_root)):
|
||||
d = os.path.join(src_root, folder)
|
||||
if not os.path.isdir(d):
|
||||
continue
|
||||
lang = folder.upper()
|
||||
if lang not in game.archives:
|
||||
skipped.append(folder) # no speech WAD of that language: positions cannot be resolved
|
||||
continue
|
||||
ref = scan_archive(lang, game.wad(lang), characters={}) # position names of this language
|
||||
by_alias = collections.defaultdict(list)
|
||||
for s in ref:
|
||||
for a in s.names():
|
||||
by_alias[a.lower()].append(s)
|
||||
for uname, who in read_user_lists(d).items():
|
||||
hits = list({id(s): s for s in by_alias.get(uname, [])}.values())
|
||||
if not hits:
|
||||
@@ -70,9 +83,27 @@ def build_characters(sounds, src_root):
|
||||
if len(hits) > 1:
|
||||
ambiguous.append(uname) # wemscan name collision: both candidates get the label
|
||||
for s in hits:
|
||||
cur = rows.setdefault(s.name, set())
|
||||
cur |= who
|
||||
return rows, unmatched, ambiguous
|
||||
f = final[s.key]
|
||||
rows[(f.name, "" if f.line else lang)] |= who
|
||||
return rows, unmatched, ambiguous, skipped
|
||||
|
||||
|
||||
def changed_since_catalog(game, out_dir, bank_snd):
|
||||
"""Keys whose original checksum in the existing catalog differs from the game files now."""
|
||||
out = []
|
||||
for arch, path in game.archives.items():
|
||||
p = os.path.join(out_dir, "%s.csv" % arch)
|
||||
if not os.path.exists(p):
|
||||
continue
|
||||
wad = Wad(path)
|
||||
for s in read_csv(p):
|
||||
if s.sha1 and s.sha1 != file_sha1(path, s.target, wad):
|
||||
out.append(s.key)
|
||||
p = os.path.join(out_dir, "%s.csv" % banks.ARCHIVE)
|
||||
if os.path.exists(p):
|
||||
old = {s.key: s.sha1 for s in read_csv(p)}
|
||||
out += [s.key for s in bank_snd if old.get(s.key) and s.sha1 and old[s.key] != s.sha1]
|
||||
return out
|
||||
|
||||
|
||||
def file_sha1(path, target, wad):
|
||||
@@ -82,6 +113,8 @@ def file_sha1(path, target, wad):
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--game")
|
||||
ap.add_argument("--accept-changes", action="store_true",
|
||||
help="the game was updated: accept sounds whose bytes differ from the current catalog")
|
||||
a = ap.parse_args()
|
||||
game = Game(a.game or default_game_dir(settings.load()))
|
||||
out_dir = catalog_dir()
|
||||
@@ -90,33 +123,42 @@ def main():
|
||||
|
||||
# 1. every package once: in-memory sounds, prefetch heads, subtitles, events (a few minutes)
|
||||
t0 = time.time()
|
||||
bank_snd, locs, sc, texts, sublocs, sub_issues, line_names = scan_text(
|
||||
bank_snd, locs, sc, texts, sublocs, sub_issues, lines = scan_text(
|
||||
game, progress=lambda k, n: print(" banks + subtitles: %d/%d files" % (k, n), flush=True)
|
||||
if k % 500 == 0 else None, characters={})
|
||||
print("packages scanned (%.0fs); %d speech sounds named by their game line" % (time.time() - t0, len(line_names)))
|
||||
st = lines.stats
|
||||
print("packages scanned (%.0fs); %d recordings in %d lines, %d of them in every language (%s); "
|
||||
"%d recordings without a line; %d relation issues"
|
||||
% (time.time() - t0, st["in_a_line"], st["lines"], st["lines_in_every_language"], "/".join(lines.langs),
|
||||
len(st["without_line"]), len(lines.issues)))
|
||||
for k, d in lines.issues[:10]:
|
||||
print(" issue:", k, d)
|
||||
|
||||
# 2. characters from the curated lists. They name ITA sounds by position (wemscan); the position of a
|
||||
# variation differs between languages, so labels go through the game's line name (valid everywhere);
|
||||
# sounds without one keep an ITA-only label.
|
||||
ref_lang = "ITA" if "ITA" in game.archives else game.languages[0]
|
||||
ref = scan_archive(ref_lang, game.wad(ref_lang), characters={})
|
||||
chars, unmatched, ambiguous = build_characters(ref, src_root)
|
||||
by_pos = {s.name: s for s in ref}
|
||||
rows = collections.defaultdict(set) # (name, language) -> characters
|
||||
for pos_name, who in chars.items():
|
||||
s = by_pos[pos_name]
|
||||
if s.key in line_names:
|
||||
rows[(line_names[s.key], "")] |= who
|
||||
else:
|
||||
rows[(pos_name, ref_lang)] |= who
|
||||
# safety: the catalog records ORIGINAL bytes. A sound that differs from the current catalog means a modified
|
||||
# game (an applied change, another tool) unless the game itself was updated -> stop before writing anything.
|
||||
changed = changed_since_catalog(game, out_dir, bank_snd)
|
||||
if changed and not a.accept_changes:
|
||||
print("STOP: %d sound(s) differ from the current catalog, e.g. %s." % (len(changed), changed[:5]))
|
||||
print("Revert all changes (ackit status / ackit revert --all) and run again; after a game update use "
|
||||
"--accept-changes.")
|
||||
sys.exit(2)
|
||||
|
||||
# 2. every speech sound with its line identity and name, then the curated speaker lists through them
|
||||
wad_sounds = {}
|
||||
for arch, path in game.archives.items():
|
||||
sounds = scan_archive(arch, path, characters={})
|
||||
wad_sounds[arch] = apply_lines(sounds, lines, lines.names) if arch in lines.langs else sounds
|
||||
final = {s.key: s for ss in wad_sounds.values() for s in ss}
|
||||
rows, unmatched, ambiguous, skipped = build_characters(game, src_root, final)
|
||||
with open(os.path.join(out_dir, "characters.csv"), "w", encoding="utf-8", newline="") as f:
|
||||
w = csv.writer(f, lineterminator="\n")
|
||||
w.writerow(["name", "character", "language"])
|
||||
for (n, lg) in sorted(rows, key=lambda r: (r[0].lower(), r[1])):
|
||||
w.writerow([n, "; ".join(sorted(rows[(n, lg)])), lg])
|
||||
print("characters.csv: %d sounds labelled (%d by line name, %d %s-only); %d unmatched names %s; %d ambiguous %s"
|
||||
% (len(rows), sum(1 for _, lg in rows if not lg), sum(1 for _, lg in rows if lg), ref_lang,
|
||||
len(unmatched), unmatched[:3], len(ambiguous), ambiguous[:3]))
|
||||
print("characters.csv: %d sounds labelled (%d by line name, %d language-only); %d unmatched names %s; "
|
||||
"%d ambiguous %s; skipped list folders without a speech WAD: %s"
|
||||
% (len(rows), sum(1 for _, lg in rows if not lg), sum(1 for _, lg in rows if lg),
|
||||
len(unmatched), unmatched[:3], len(ambiguous), ambiguous[:3], skipped))
|
||||
|
||||
def charmap(lang):
|
||||
return {n.lower(): "; ".join(sorted(c)) for (n, lg), c in rows.items() if not lg or lg == lang}
|
||||
@@ -124,7 +166,7 @@ def main():
|
||||
index = {"built_from": "unmodified game files", "archives": {}}
|
||||
for arch, path in game.archives.items():
|
||||
cm = charmap(arch)
|
||||
sounds = apply_line_names(scan_archive(arch, path, characters={}), line_names)
|
||||
sounds = wad_sounds[arch]
|
||||
for s in sounds:
|
||||
s.character = character_of(s, cm)
|
||||
wad = Wad(path)
|
||||
@@ -170,6 +212,11 @@ def main():
|
||||
"speech_lines": len({s.speech for s in any_lang}),
|
||||
"linked": {lang: sum(1 for s in rows if s.sound) for lang, rows in sorted(texts.items())},
|
||||
"copies": len(sublocs), "issues": ["%s: %s" % i for i in sub_issues]}
|
||||
index["archives"]["LINES"] = {
|
||||
"languages": lines.langs, "recordings": st["recordings"], "lines": st["lines"],
|
||||
"lines_in_every_language": st["lines_in_every_language"],
|
||||
"lines_missing_a_language": st["lines_missing_a_language"], "without_line": st["without_line"],
|
||||
"issues": ["%s: %s" % i for i in lines.issues]}
|
||||
print("SUB_*.csv: %d languages, %d subtitle strings, %d copies"
|
||||
% (len(texts), sum(len(r) for r in texts.values()), len(sublocs)))
|
||||
# flatten for catalog.load_archive
|
||||
|
||||
@@ -29,9 +29,6 @@ def sha(p):
|
||||
|
||||
def main():
|
||||
from PySide6.QtWidgets import QApplication
|
||||
from ackit import audio
|
||||
from ackit.inject import pack_bnk_path, parse_target
|
||||
from ackit.verify import ingame
|
||||
from app.mainwindow import MainWindow
|
||||
from app.workers import Runner
|
||||
|
||||
@@ -45,6 +42,21 @@ def main():
|
||||
ws = w.ws
|
||||
assert ws, "game not loaded"
|
||||
ok = True
|
||||
try:
|
||||
ok = run(w, ws, errors)
|
||||
finally: # never leave a change behind, whatever failed
|
||||
ws.revert_all()
|
||||
ws.texts.revert_all()
|
||||
print("E2E EDITOR %s" % ("PASS" if ok else "FAIL"))
|
||||
w.close()
|
||||
return 0 if ok else 1
|
||||
|
||||
|
||||
def run(w, ws, errors):
|
||||
from ackit import audio
|
||||
from ackit.inject import pack_bnk_path, parse_target
|
||||
from ackit.verify import ingame
|
||||
ok = True
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
# --- music (stream pack, resized) ---
|
||||
s = ws.sound("Music_393BC53A")
|
||||
@@ -71,10 +83,11 @@ def main():
|
||||
ok &= same
|
||||
|
||||
# --- speech line (in place) ---
|
||||
t = ws.sound("Speech_TAPE_JOKER_1_4")
|
||||
t = ws.sound("Speech_TAPE_JOKER_1#4") # 4th WEM of that entry in this language's WAD
|
||||
twad = ws.wad_path(t)
|
||||
tb = sha(twad)
|
||||
w.select_name("Speech_TAPE_JOKER_1_4") or (w.clear_filters(), w.select_name("Speech_TAPE_JOKER_1_4"))
|
||||
w.clear_filters()
|
||||
assert w.select_name(t.name)
|
||||
y, ysr = audio.decode_wem(ws.original_bytes(t))
|
||||
line = os.path.join(td, "line.wav")
|
||||
audio.save_wav(line, y[::-1], ysr) # reversed speech, same length
|
||||
@@ -86,9 +99,7 @@ def main():
|
||||
same2 = sha(twad) == tb
|
||||
print("speech line applied (WAD changed: %s), reverted byte-identical: %s" % (changed, same2))
|
||||
ok &= changed and same2
|
||||
print("E2E EDITOR %s" % ("PASS" if ok else "FAIL"))
|
||||
w.close()
|
||||
return 0 if ok else 1
|
||||
return ok
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
248
tools/e2e_features.py
Normal file
248
tools/e2e_features.py
Normal file
@@ -0,0 +1,248 @@
|
||||
"""Real-game write round trips for the mission-3 features (no game launch; reverts everything itself).
|
||||
|
||||
python tools/e2e_features.py [--game DIR] [--lang ITA,INT,... | --lang all]
|
||||
|
||||
Runs the checks for each language (default: the game's language; `all` = every subtitle/menu language). A
|
||||
language without its own voices (ESM, POL, POR, RUS, JPN, KOR) uses the INT voices, like the game and the editor.
|
||||
For every check: the SHA-1 of each touched file is taken, the change is applied through Workspace (the code path
|
||||
of the editor and the CLI), read back from the game files, reverted, and the files must be byte-identical again.
|
||||
1. in-memory sound with the most copies (every copy written; once, it is shared by all languages)
|
||||
2. localized in-memory speech line of the language's voices
|
||||
3. speech line of the language's voices with prefetch heads (WAD + every head copy)
|
||||
4. subtitle with several copies (RDialogueEvent), and a scripted subtitle (RSubtitle, Startup.upk)
|
||||
5. menu text
|
||||
6. sound + subtitle in the same package, reverted one at a time
|
||||
7. mod package made with the language's voices and imported into another voice language (INT; FRA for INT):
|
||||
matched by the game's line identity, so the same line (not the same WAD position) is replaced; a text key
|
||||
names its language, so the edited subtitle stays that language's subtitle (the other language's is untouched)
|
||||
Uses temporary originals stores (the user's store and settings are not touched), deleted at the end when every
|
||||
change was reverted (kept, with a warning, otherwise). Whatever happens (a failed check, an exception), every
|
||||
change is reverted before the tool exits; a sound that is not in its original state is never written.
|
||||
"""
|
||||
import argparse
|
||||
import hashlib
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
sys.path.insert(0, ROOT)
|
||||
|
||||
import numpy as np # noqa: E402
|
||||
|
||||
from ackit import banks, settings, subtitles # noqa: E402
|
||||
from ackit.game import Game, default_game_dir # noqa: E402
|
||||
from ackit.modpack import export_mod, import_mod, match_mod # noqa: E402
|
||||
from ackit.workspace import Workspace, MODIFIED, ORIGINAL # noqa: E402
|
||||
|
||||
RESULTS = []
|
||||
SPACES = [] # every Workspace created: reverted in main()'s finally block
|
||||
|
||||
|
||||
def sha(p):
|
||||
h = hashlib.sha1()
|
||||
with open(p, "rb") as f:
|
||||
for c in iter(lambda: f.read(1 << 22), b""):
|
||||
h.update(c)
|
||||
return h.hexdigest()
|
||||
|
||||
|
||||
def check(name, ok, detail=""):
|
||||
RESULTS.append((name, bool(ok)))
|
||||
print("%-62s %s %s" % (name, "PASS" if ok else "FAIL", detail))
|
||||
|
||||
|
||||
def tone(seconds, freq):
|
||||
t = np.arange(int(seconds * 48000)) / 48000.0
|
||||
return (0.3 * np.sin(2 * np.pi * freq * t)).astype(np.float32)[:, None], 48000
|
||||
|
||||
|
||||
def snapshot(paths):
|
||||
return {p: sha(p) for p in sorted(set(paths))}
|
||||
|
||||
|
||||
def copies_of(ws, s):
|
||||
out = []
|
||||
for l in ws.locations(s):
|
||||
b = banks.open_file(ws.bank_path(l.file)).read(l.offset, l.size)
|
||||
out.append((l, b))
|
||||
return out
|
||||
|
||||
|
||||
def sound_round_trip(ws, s, label):
|
||||
if ws.state(s) != ORIGINAL:
|
||||
check(label, False, "(%s is not original: %s; nothing written)" % (s.name, ws.state(s)))
|
||||
return
|
||||
wem, _ = ws.prepare(s, *tone(min(max(s.seconds, 0.3), 1.0), 700))
|
||||
files = ws.files(s)
|
||||
before = snapshot(files)
|
||||
ws.apply([(s, wem, {"source": "e2e"})])
|
||||
ok = ws.state(s) == MODIFIED and ws.current_bytes(s) == wem
|
||||
for l, b in copies_of(ws, s):
|
||||
if l.kind == banks.MEMORY:
|
||||
ok &= b == wem + bytes(l.size - len(wem))
|
||||
else: # prefetch head of a WAD line (stored zero-padded to its size)
|
||||
full = wem + bytes(max(0, ws.store.get(s.key)["orig_size"] - len(wem)))
|
||||
ok &= b == (full + bytes(l.size))[:l.size]
|
||||
ws.revert([s])
|
||||
ok &= ws.state(s) == ORIGINAL and snapshot(files) == before and not ws.store.entries
|
||||
check(label, ok, "(%s, %d files, %d copies/heads)" % (s.name, len(files), len(ws.locations(s))))
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--game")
|
||||
ap.add_argument("--lang", help="comma-separated languages, or 'all' (default: the game's language)")
|
||||
a = ap.parse_args()
|
||||
game = Game(a.game or default_game_dir(settings.load()))
|
||||
td = tempfile.mkdtemp()
|
||||
try:
|
||||
first = True
|
||||
for lang in languages(game, a.lang, td):
|
||||
voices = lang if lang in game.languages else "INT"
|
||||
ws = Workspace(game, os.path.join(td, "store_" + lang), [voices, "SFX", "BANK"])
|
||||
SPACES.append(ws)
|
||||
print("\n== %s (voices %s): %d sounds" % (lang, voices, len(ws.catalog.sounds)))
|
||||
run_checks(game, lang, voices, td, ws, first)
|
||||
first = False
|
||||
finally:
|
||||
clean = True
|
||||
for w in SPACES: # never leave a change behind
|
||||
w.revert_all()
|
||||
w.texts.revert_all()
|
||||
if w.store.entries or w.texts.records():
|
||||
clean = False
|
||||
print("WARNING: could not revert everything; store: %s" % w.store.root)
|
||||
if clean: # the stores hold full copies of game files (GBs): drop them
|
||||
shutil.rmtree(td, ignore_errors=True)
|
||||
print("\n%d/%d PASS" % (sum(ok for _, ok in RESULTS), len(RESULTS)))
|
||||
return 0 if RESULTS and all(ok for _, ok in RESULTS) else 1
|
||||
|
||||
|
||||
def languages(game, arg, td):
|
||||
if not arg:
|
||||
return [game.language() or "INT"]
|
||||
if arg.lower() != "all":
|
||||
return [x.strip().upper() for x in arg.split(",") if x.strip()]
|
||||
tx = Workspace(game, os.path.join(td, "store_list"), [game.languages[0]]).texts
|
||||
return sorted(set(game.languages) | set(tx.subtitle_languages()) | set(tx.menu_languages()))
|
||||
|
||||
|
||||
def run_checks(game, lang, voices, td, ws, first):
|
||||
bank = [s for s in ws.catalog.sounds if s.archive == "BANK" and s.size >= 2500]
|
||||
if first:
|
||||
sound_round_trip(ws, max((s for s in bank if not s.language), key=lambda s: len(ws.locations(s))),
|
||||
"1 in-memory sound, every copy")
|
||||
loc = [s for s in bank if s.language == voices]
|
||||
sound_round_trip(ws, max(loc, key=lambda s: len(ws.locations(s))),
|
||||
"2 %s localized in-memory speech line (%s)" % (lang, voices))
|
||||
heads = [ws.catalog.by_key[k] for k in ws.catalog.heads if k in ws.catalog.by_key
|
||||
and ws.catalog.by_key[k].archive == voices]
|
||||
sound_round_trip(ws, max(heads, key=lambda s: len(ws.locations(s))),
|
||||
"3 %s speech line with prefetch heads" % lang)
|
||||
|
||||
tx = ws.texts
|
||||
subs = tx.subtitles(lang)
|
||||
|
||||
def text_round_trip(key, new, label):
|
||||
files = tx.files(key)
|
||||
before = snapshot(files)
|
||||
tx.apply({key: new})
|
||||
ok = tx.current(key) == new and tx.state(key) == MODIFIED
|
||||
for l in tx.subtitle_locations(key) if key.startswith("SUB:") else []:
|
||||
b = banks.open_file(ws.bank_path(l.file)).read(l.pos, l.size)
|
||||
ok &= subtitles.read_fstring(b, 0)[0] == new
|
||||
tx.revert([key])
|
||||
ok &= snapshot(files) == before and not tx.records()
|
||||
check(label, ok, "(%s, %d files)" % (key[:60], len(files)))
|
||||
|
||||
multi = max((s for s in subs if s.room >= 8), key=lambda s: len(tx.subtitle_locations(subtitles.key(lang, s.speech, s.line))))
|
||||
text_round_trip(subtitles.key(lang, multi.speech, multi.line), "E2E test.", "4a %s subtitle with several copies" % lang)
|
||||
rsub = next((s for s in subs if s.room >= 10 and
|
||||
{l.file for l in tx.subtitle_locations(subtitles.key(lang, s.speech, s.line))} == {"Startup.upk"}), None)
|
||||
if rsub:
|
||||
text_round_trip(subtitles.key(lang, rsub.speech, rsub.line), "E2E radio.", "4b %s scripted subtitle (RSubtitle)" % lang)
|
||||
else:
|
||||
check("4b %s scripted subtitle (RSubtitle)" % lang, False, "no scripted subtitle found in Startup.upk")
|
||||
e = next(x for x in tx.menu(lang) if x.file.upper().startswith("FRONTEND"))
|
||||
text_round_trip("MENU:" + e.key, "E2E " + e.value, "5 %s menu text" % lang)
|
||||
|
||||
# 6. sound + subtitle in one package
|
||||
sub_files = {}
|
||||
for s in subs:
|
||||
if s.room < len("Shared package."):
|
||||
continue
|
||||
for l in tx.subtitle_locations(subtitles.key(lang, s.speech, s.line)):
|
||||
sub_files.setdefault(l.file, subtitles.key(lang, s.speech, s.line))
|
||||
pair = next(((s, sub_files[l.file]) for s in bank if not s.language for l in ws.locations(s)
|
||||
if l.file in sub_files and len(ws.locations(s)) <= 3), None)
|
||||
if pair:
|
||||
s, key = pair
|
||||
files = set(ws.files(s)) | set(tx.files(key))
|
||||
before = snapshot(files)
|
||||
wem, _ = ws.prepare(s, *tone(0.3, 900))
|
||||
ws.apply([(s, wem, {})])
|
||||
tx.apply({key: "Shared package."})
|
||||
ok = ws.state(s) == MODIFIED and tx.current(key) == "Shared package."
|
||||
ws.revert([s])
|
||||
ok &= ws.state(s) == ORIGINAL and tx.current(key) == "Shared package."
|
||||
tx.revert([key])
|
||||
ok &= snapshot(files) == before
|
||||
check("6 %s sound + subtitle in one package, reverted separately" % lang, ok, "(%s + %s)" % (s.name, key[:40]))
|
||||
else:
|
||||
check("6 %s sound + subtitle in one package" % lang, False, "no such package found")
|
||||
|
||||
# 7. cross-language mod: the same game line, not the same WAD position
|
||||
from ackit.catalog import read_csv
|
||||
from ackit.paths import catalog_dir
|
||||
other = "FRA" if voices == "INT" else "INT"
|
||||
other_target = {s.name: s.target for s in read_csv(os.path.join(catalog_dir(), other + ".csv"))}
|
||||
other_subs = {(s.speech, s.line) for s in tx.subtitles(other)}
|
||||
by_sound = {}
|
||||
for s in subs:
|
||||
for k in s.sounds:
|
||||
by_sound.setdefault(k, s)
|
||||
line = next((s for s in ws.catalog.sounds if s.archive == voices and ":" in s.target
|
||||
and s.key in by_sound and by_sound[s.key].room >= 9
|
||||
and (by_sound[s.key].speech, by_sound[s.key].line) in other_subs # the line exists there too
|
||||
and other_target.get(s.name, s.target) != s.target), None) # at another position there
|
||||
if line is None:
|
||||
check("7 mod %s -> %s" % (lang, other), False, "no line found at another position in %s" % other)
|
||||
return
|
||||
sub = by_sound[line.key]
|
||||
sub_key = subtitles.key(lang, sub.speech, sub.line)
|
||||
if ws.state(line) != ORIGINAL or tx.state(sub_key) != ORIGINAL:
|
||||
check("7 mod %s -> %s" % (lang, other), False, "(%s is not original; nothing written)" % line.name)
|
||||
return
|
||||
wem, _ = ws.prepare(line, *tone(1.0, 500))
|
||||
ws.apply([(line, wem, {})])
|
||||
tx.apply({sub_key: "Mod text."})
|
||||
pkg = os.path.join(td, "e2e_%s.acmod" % lang)
|
||||
export_mod(ws, pkg, ws.modified_keys(), "e2e", texts=tx.modified())
|
||||
ws.revert_all()
|
||||
tx.revert_all()
|
||||
wo = Workspace(game, os.path.join(td, "store_%s_%s" % (lang, other)), [other, "SFX", "BANK"])
|
||||
SPACES.append(wo)
|
||||
_, items = match_mod(wo, pkg)
|
||||
target = items[0].sound
|
||||
same_line = target is not None and target.name == line.name
|
||||
moved = target is not None and target.target != line.target # positions differ between languages
|
||||
other_sub = subtitles.key(other, sub.speech, sub.line)
|
||||
files = set(wo.files(target)) if target else set()
|
||||
files |= set(wo.texts.files(other_sub)) | set(wo.texts.files(sub_key))
|
||||
before = snapshot(files)
|
||||
other_text = wo.texts.current(other_sub)
|
||||
res, texts = import_mod(wo, pkg)
|
||||
ok = same_line and wo.state(target) == MODIFIED
|
||||
ok &= [k for k, _, p in texts if not p] == [sub_key] and wo.texts.current(sub_key) == "Mod text."
|
||||
ok &= wo.texts.current(other_sub) == other_text
|
||||
wo.revert_all()
|
||||
wo.texts.revert_all()
|
||||
ok &= snapshot(files) == before
|
||||
check("7 mod %s -> %s: same game line%s" % (lang, other, " (another WAD position)" if moved else ""), ok,
|
||||
"(%s: %s:%s -> %s:%s)" % (line.name, voices, line.target, other, target.target if target else "-"))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user