Game line names across languages; RSubtitle; editor in three columns
This commit is contained in:
@@ -22,6 +22,8 @@ from collections import defaultdict, namedtuple
|
||||
from .upk import Package
|
||||
|
||||
CLASS = "RDialogueEvent"
|
||||
SUBTITLE_CLASS, LOOKUP_CLASS = "RSubtitle", "RSubtitleLookup" # scripted radio/encryption subtitles
|
||||
UNRESOLVED = "#lookup:%08X" # speech of an RSubtitle until its lookup key is matched to a line name
|
||||
T_STRUCT, T_STRING, T_OBJECT = 0x09, 0x0D, 0x10
|
||||
|
||||
# one subtitle string inside a package: pos = absolute uncompressed offset of the FString length field,
|
||||
@@ -109,9 +111,14 @@ def _parse_lines(b, o, end, base, lang):
|
||||
|
||||
|
||||
def parse_dialogue(b, base, names_idx):
|
||||
"""Fields of one RDialogueEvent export (bytes `b` starting at uncompressed offset `base`)."""
|
||||
ls, sub = names_idx
|
||||
i = b.find(struct.pack("<II", ls, 0))
|
||||
"""Fields of one dialogue/subtitle export (bytes `b` starting at uncompressed offset `base`).
|
||||
names_idx: (name indices of the per-language array: LocalizedSubtitles or ArraySubtitles, of Subtitles)."""
|
||||
arrays, sub = names_idx
|
||||
i = -1
|
||||
for ls in arrays:
|
||||
i = b.find(struct.pack("<II", ls, 0))
|
||||
if i >= 4 and struct.unpack_from("<H", b, i - 4)[0] == T_STRUCT:
|
||||
break
|
||||
if i < 4 or struct.unpack_from("<H", b, i - 4)[0] != T_STRUCT:
|
||||
return []
|
||||
size = struct.unpack_from("<i", b, i + 8)[0]
|
||||
@@ -168,6 +175,24 @@ class Collector:
|
||||
except (SubtitleError, struct.error, ValueError, IndexError) as e:
|
||||
self.issues.append(("subtitles", "%s: %s" % (rel, e)))
|
||||
|
||||
PREFIXES = ("", "RADIO_SCAN_", "RADIO_", "TV_", "CH_ZERO_", "GCPD_", "CHAR_", "SEQUENCE_")
|
||||
|
||||
def resolve(self, line_names):
|
||||
"""Give RSubtitle objects their speech line name: the name whose lookup_key equals the object's key,
|
||||
from `line_names` or built from the object's own name (Speech_ + prefix + name; exact hash match)."""
|
||||
by_key = {lookup_key(n): n for n in line_names}
|
||||
for sp in [s for s in self.dialogues if s.startswith("#lookup:")]:
|
||||
key = int(sp.split(":")[1], 16)
|
||||
name = by_key.get(key)
|
||||
if not name:
|
||||
exp = self.dialogues[sp][0][1].export
|
||||
name = next(("Speech_" + p + exp for p in self.PREFIXES if lookup_key("Speech_" + p + exp) == key), None)
|
||||
copies = self.dialogues.pop(sp)
|
||||
if not name:
|
||||
self.issues.append(("subtitles", "no speech line for subtitle %s" % copies[0][1].export))
|
||||
name = copies[0][1].export
|
||||
self.dialogues[name].extend((rel, d._replace(speech=name)) for rel, d in copies)
|
||||
|
||||
|
||||
# --- catalog files: SUB_<LANG>.csv (texts) + SUB_locations.csv.gz (every copy) ------------------
|
||||
LOCATIONS = "SUB_locations.csv.gz"
|
||||
@@ -236,20 +261,56 @@ def read_locations(path, lang=None):
|
||||
return dict(out)
|
||||
|
||||
|
||||
def lookup_key(line_name):
|
||||
"""Key of a speech line in the RSubtitleLookup table: FNV-1 of its name without 'Speech_'."""
|
||||
from .bnk import fnv1
|
||||
return fnv1(line_name[7:] if line_name.lower().startswith("speech_") else line_name)
|
||||
|
||||
|
||||
def _lookup_table(b):
|
||||
"""RSubtitleLookup export data -> {export index: key}: array of {int key @0, object @4}."""
|
||||
count = struct.unpack_from("<I", b, 24)[0]
|
||||
out, o = {}, 28
|
||||
for _ in range(count):
|
||||
if b[o:o + 4] != b"\x02\x00\x00\x00" or b[o + 8:o + 12] != b"\x10\x00\x04\x00":
|
||||
raise SubtitleError("unexpected subtitle lookup layout")
|
||||
key, ref = struct.unpack_from("<Ii", b, o + 4)[0], struct.unpack_from("<i", b, o + 12)[0]
|
||||
out[ref] = key
|
||||
o += 18
|
||||
return out
|
||||
|
||||
|
||||
def scan_package(pkg, data=None, start=None, tables=None):
|
||||
"""[Dialogue] of one package (empty if it has none)."""
|
||||
"""[Dialogue] of one package (empty if it has none): RDialogueEvent objects (speech = the referenced
|
||||
speech event) and RSubtitle objects (speech = UNRESOLVED % lookup key, see Collector.resolve)."""
|
||||
if data is None:
|
||||
start, data = pkg.read_all()
|
||||
names, imports, exports = tables or pkg.tables(data, start)
|
||||
cls = {-(i + 1) for i, (c, o, _) in enumerate(imports) if c == "Class" and o == CLASS}
|
||||
if not cls or "LocalizedSubtitles" not in names or "Subtitles" not in names:
|
||||
by_class = {}
|
||||
for i, (c, o, _) in enumerate(imports):
|
||||
if c == "Class" and o in (CLASS, SUBTITLE_CLASS, LOOKUP_CLASS):
|
||||
by_class[-(i + 1)] = o
|
||||
arrays = [names.index(a) for a in ("LocalizedSubtitles", "ArraySubtitles") if a in names]
|
||||
if not by_class or not arrays or "Subtitles" not in names:
|
||||
return []
|
||||
idx = (names.index("LocalizedSubtitles"), names.index("Subtitles"))
|
||||
idx = (arrays, names.index("Subtitles"))
|
||||
keys = {}
|
||||
for e in exports:
|
||||
if by_class.get(e.cls) == LOOKUP_CLASS and e.size > 0:
|
||||
keys.update(_lookup_table(data[e.offset - start:e.offset - start + e.size]))
|
||||
out = []
|
||||
for e in exports:
|
||||
if e.cls in cls and e.size > 0:
|
||||
kind = by_class.get(e.cls)
|
||||
if kind in (CLASS, SUBTITLE_CLASS) and e.size > 0:
|
||||
b = data[e.offset - start:e.offset - start + e.size]
|
||||
fields = parse_dialogue(b, e.offset, idx)
|
||||
if fields:
|
||||
out.append(Dialogue(_speech_ref(b, exports, imports), e.name, fields))
|
||||
if not fields:
|
||||
continue
|
||||
if kind == CLASS:
|
||||
speech = _speech_ref(b, exports, imports)
|
||||
elif e.index in keys:
|
||||
speech = UNRESOLVED % keys[e.index]
|
||||
else:
|
||||
speech = e.name # not in a lookup table: keep the object's name
|
||||
out.append(Dialogue(speech, e.name, fields))
|
||||
return out
|
||||
|
||||
Reference in New Issue
Block a user