Version 3.2.1: whole bank copies of voice lines, unique names, docs figures checked by tools/check_docs.py

This commit is contained in:
2026-10-10 19:30:06 +02:00
parent 1586abb8e9
commit cac52e7597
29 changed files with 919 additions and 188 deletions

View File

@@ -234,10 +234,11 @@ def scan_archive(archive, wad_path, characters=None, progress=None):
_CAMEL = re.compile(r"(?<=[a-z])(?=[A-Z])")
def bank_sounds(sc, characters=None):
"""[Sound] of the in-memory sounds found by `banks.scan` (prefetch heads belong to WAD sounds)."""
def bank_sounds(sc, characters=None, reserved=()):
"""[Sound] of the in-memory sounds found by `banks.scan` (prefetch heads belong to WAD sounds). `reserved`:
lower-case names taken by other archives (sfx_names)."""
characters = characters if characters is not None else load_characters()
names = banks.media_names(sc)
names = banks.media_names(sc, reserved)
every = set().union(*(e.get("langs", set()) for e in sc.media.values())) - {""}
out = []
for m, e in sc.media.items():
@@ -275,13 +276,45 @@ def derive_banks(game, sc, characters=None):
"""(bank sounds, locations, Lines) from a Scan: the lines, the prefetch heads' lines, the in-memory sounds
with their line identities and names."""
lines = build_lines(game, sc)
mark_heads(sc, whole_recordings(sc, lines))
locs = [l._replace(sound=lines.head_key(l)) if l.kind == banks.PREFETCH_HEAD else l for l in sc.locations]
snd = bank_sounds(sc, characters)
snd = bank_sounds(sc, characters, sfx_names(game))
lines.names = line_names(game, sc, lines, {s.key: s.name for s in snd})
apply_lines(snd, lines, lines.names)
return snd, locs, lines
def whole_recordings(sc, lines):
"""Media stored in memory that are WAD recordings of their bank's language (the same media ID, identical
bytes: 49 in the game, e.g. game-over taunts). Like prefetch heads, these copies belong to the WAD line
and are written with it; they are not sounds of their own."""
keys = collections.defaultdict(list)
for l in sc.locations:
if l.kind == banks.MEMORY:
k = lines.head_key(l)
keys[l.media].append(bool(k) and not k.startswith(banks.ARCHIVE + ":"))
return {m for m, wad in keys.items() if all(wad)}
def mark_heads(sc, media):
"""Treat the bank copies of `media` as heads of WAD recordings (see whole_recordings)."""
if not media:
return
for m in media:
if m in sc.media:
sc.media[m]["kinds"] = {banks.PREFETCH_HEAD}
sc.prefetch.add(m)
sc.locations = [l._replace(kind=banks.PREFETCH_HEAD) if l.media in media else l for l in sc.locations]
def sfx_names(game):
"""Lower-case names and aliases of the SFX WAD's sounds: in-memory sounds of the same bank must not take
them (`<Bank>_<n>` is the position name of the bank's streamed sounds too)."""
if "SFX" not in game.archives:
return set()
return {n.lower() for s in scan_archive("SFX", game.wad("SFX"), characters={}) for n in s.names()}
def line_names(game, sc, lines, bank_names=None):
"""{line id: name} (lines.line_names). Lines no event names keep their English recording's name: the
position-based name in the INT WAD (`NAME_i`, or `NAME#i` where that is taken) or the bank-numbered name."""
@@ -378,6 +411,10 @@ def scan_text(game, progress=None, characters=None):
sc = banks.scan(game.cooked, progress, visit=col)
names = {n for n in sc.names if n.lower().startswith("speech_")}
names |= {n for n, _ in banks.media_names(sc).values() if n.lower().startswith("speech_")}
lookup = [n for n in col.dialogues if n.startswith("#lookup:")]
sc.subtitle_objects = {"dialogue_objects": sum(len(v) for n, v in col.dialogues.items() if n not in lookup),
"dialogue_lines": len(col.dialogues) - len(lookup),
"scripted_objects": sum(len(col.dialogues[n]) for n in lookup)}
col.resolve(names)
# the subtitle objects name their speech events too: an event ID is the FNV-1 hash of its name, so these
# names find events no package name table lists (e.g. Vicki Vale's radio reports)
@@ -519,6 +556,11 @@ class Catalog:
key = os.path.splitext(os.path.basename(name))[0].lower() if re.search(r"\.\w{2,4}$", name) else name.lower()
hits = self._by_name.get(key, [])
uniq = list({id(s): s for s in hits}.values())
if len(uniq) > 1:
# a name wins over another sound's alias: a line named after its INT position (NAME#i) can equal
# the position alias of another recording in another language's WAD
own = [s for s in uniq if s.name.lower() == key]
uniq = own if len(own) == 1 else uniq
if len(uniq) == 1:
return uniq[0]
if len(uniq) > 1: