Files
UnheardCity/docs/formats/subtitles.md

3.9 KiB

Subtitles (RDialogueEvent objects in the packages)

There are no subtitle files: every speech line has a dialogue object (class RDialogueEvent, Rocksteady) inside the UE3 packages (upk.md), holding the subtitle text of all 11 languages. ackit reads them with ackit/subtitles.py and writes them through Workspace.texts (ackit/texts.py).

Inventory (buildid 3718300)

  • 14,743 dialogue objects in the packages for 9,016 speech lines (a line's object is copied into every package that uses it; all copies are identical).
  • 11 languages: INT, FRA, ITA, DEU, ESN, ESM, POL, POR, RUS, JPN, KOR (the speech WADs exist only for INT, FRA, ITA, DEU, ESN; the other languages play the INT voices with their own subtitles).
  • ~9,400 subtitle strings per language: 94 % of the subtitles have one line, the rest up to 5 lines.
  • Strings are stored as cp1252 (positive FString length) or UTF-16 (negative length): RUS, POL, JPN, KOR and some INT/ESN/FRA/DEU strings are UTF-16.

Scripted subtitles (RSubtitle)

41 more subtitles (radio and encryption-card lines) are RSubtitle objects in Startup.upk: the same layout, with the per-language array named ArraySubtitles and a float (duration) at 0x2C instead of a speech reference. RSubtitleLookup (Startup.upk, Lookups) is an array of {int key @0, object @4}, key = FNV-1 of the speech line name without Speech_ (e.g. CH_ZERO_CH0_ENCRYPTION_CARD2_01), which links each RSubtitle exactly to its line. ackit reads and writes both kinds the same way.

Object layout (compact tagged properties)

The export data is a list of properties u16 type, u16 field offset, value, ended by u16 0. Types seen: 0x10 object reference (i32 export/import index), 0x0D FString, 0x09 struct/array (name i32+i32 = struct or property name from the name table, i32 byte size, i32 array index, then size bytes), 0x04 4-byte value, 0x03 bool, 0x01 enum. Relevant properties:

field content
0x10 @ 0x2C the speech event this line plays, e.g. export Speech_CHURCH_B1_Ch2_POST_Explosion_BM_Cash_04
0x0D @ 0x34 speaker, e.g. Batman
0x09 LocalizedSubtitles u32 count, then per language: 0D 00 00 00 FString language code, 09 00 0C 00 Subtitles struct, 00 00
Subtitles u32 line count, then per line: 0D 00 00 00 FString text, 04 00 0C 00 4-byte value, 00 00

The sizes add up exactly in all 14,743 objects (checked by the parser: SubtitleError otherwise).

The speech event name is hashed (FNV-1) to the Wwise event ID, followed through the soundbanks (event -> action -> containers -> Sound) to the streamed media, and that media ID is looked up in the per-language BKPK tables (banks.md) to find the WAD entry and variation of each language. This is exact; the _NN suffix of the event name does not match the variation order inside the WAD entry (WEMs are stored in media-ID order). Lines kept in memory link to the localized BANK sound of their language. Linked: ~99 % of the strings (e.g. INT 9,265 / 9,379); the rest have no event that reaches a sound (unused or placeholder lines such as ..._TEMP_29) and stay editable without a sound link. Event names with a number (Speech_X_04) exist only as export names, not in the name table.

How ackit writes them

  • In place, into every copy (data/catalog/SUB_locations.csv.gz): the new text replaces the FString's bytes and is padded with NUL characters, keeping the length field. The game stops at the first NUL, so no size field, export or package layout changes. Limit: the new text must fit the original string's bytes (cp1252: one byte per character; UTF-16: two) - shown as "room" in the CLI and the editor.
  • A cp1252 string can only hold cp1252 characters; UTF-16 strings can hold any character.
  • Packages are rebuilt from the store's original copy + all applied sound and text edits (docs/decisions.md).