Version 3.1.0: voice lines linked across all languages from the game data
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
## 1. Stack: Python + Qt (PySide6), packaged with PyInstaller
|
||||
- **Reuse:** the toolkit (`ackit`) is Python and proven against the real game; the editor imports it unchanged.
|
||||
- **Cross-platform GUI:** Qt Widgets via PySide6 (LGPL-3.0, official Qt for Python) looks native on
|
||||
Windows and Linux, has a fast model/view table for 11,000+ rows, and runs offscreen for automated GUI tests.
|
||||
Windows and Linux, has a fast model/view table for 20,000+ rows, and runs offscreen for automated GUI tests.
|
||||
- **Audio I/O:** Qt Multimedia (`QMediaDevices`, `QAudioSink`, `QAudioSource`) for playback, recording and
|
||||
device selection. It ships inside the PySide6 wheels with its own backends (WASAPI on Windows,
|
||||
PulseAudio/PipeWire on Linux), so unlike PortAudio/`sounddevice` nothing has to be installed on Linux.
|
||||
@@ -42,6 +42,7 @@ loopback); it is a developer tool and not part of the editor.
|
||||
every record. Stream-pack padding bytes are stored so pack reverts are byte-identical.
|
||||
- **Full-file fallback:** before the first change to a WAD (and its pack index), one verified full copy is kept
|
||||
in the store (`full/`), default on, can be disabled. Used by "Restore from full backup" if the store is damaged.
|
||||
Soundbank files, packages and localization files always get this copy (they are rebuilt from it, 4b/4c).
|
||||
- **Why the user data folder:** it survives app updates and reinstalls, needs no write access next to the game,
|
||||
and works the same on Linux/Proton. Steam never touches it.
|
||||
- **Safe writes:** every WAD is rebuilt into a temp file and atomically renamed over the original, pack indexes
|
||||
@@ -53,7 +54,8 @@ loopback); it is a developer tool and not part of the editor.
|
||||
|
||||
## 4. Size rule
|
||||
Speech/ambience variations are addressed by offset+size from BKPK tables inside compressed `.upk` packages,
|
||||
which ackit does not rewrite. Replacements are written in place and must not exceed the original WEM's size;
|
||||
which ackit leaves unchanged (it patches packages only in place). Replacements are written in place and must not
|
||||
exceed the original WEM's size;
|
||||
the encoder lowers quality, then sample rate, to fit, and refuses otherwise. Music stream packs have a loose
|
||||
index (`SFX\*.bnk`) that ackit rewrites, so music can be any length. In-memory sounds follow the same rule
|
||||
(below). Lifting the limit would mean re-laying out the uncompressed packages (export offsets, BKPK/DIDX
|
||||
@@ -76,8 +78,12 @@ tables): high risk for little gain, not done.
|
||||
- **Prefetch heads:** 396 speech lines (about 79 per language) have their first bytes copied into banks; a
|
||||
replacement of such a line rewrites those heads too, otherwise the start would play the old line.
|
||||
- **Names:** the hash list names none of these media. Names come from Wwise events whose names are in the
|
||||
packages' name tables (FNV-1 of the name = event ID), following event -> action -> containers -> sounds
|
||||
(6,416 sounds); the rest are numbered per bank. Categories use the bank name.
|
||||
packages' name and export tables (FNV-1 of the name = event ID), following event -> action -> containers ->
|
||||
sounds (6,456 sounds); the rest are numbered per bank. Categories use the bank name. A localized speech line
|
||||
has the same name in every language (its line, section 5).
|
||||
- **Localized speech:** speech banks hold one copy per voice language (4d); every media is labelled with the
|
||||
language(s) whose banks hold it (a few recordings are shared by several languages) and the catalog shows
|
||||
only the selected language's lines.
|
||||
|
||||
## 4c. Subtitles and menu text
|
||||
- **Subtitles** are written in place into every copy of the dialogue object, NUL-padded inside the original
|
||||
@@ -85,8 +91,11 @@ tables): high risk for little gain, not done.
|
||||
string's length ("room"). Moving the object to the end of the package (new export offset/size and an extra
|
||||
chunk) would lift the limit, but no dialogue is loaded on the title screen, so the result could not be
|
||||
checked automatically in the game: not done.
|
||||
- **Links** subtitle -> speech sound come from the soundbanks (event -> ... -> per-language media -> WAD), not
|
||||
from names: the `_NN` suffixes do not follow the WEM order inside WAD entries.
|
||||
- **Links** subtitle -> speech sound come from the soundbanks (event -> ... -> per-language media -> WAD line
|
||||
or localized in-memory line), not from names: the `_NN` suffixes do not follow the WEM order inside WAD
|
||||
entries. Scripted subtitles (`RSubtitle`) are linked through the game's own lookup table (FNV-1 of the line
|
||||
name without `Speech_`). The subtitle objects' names also find events that no package name table lists, and
|
||||
an event that plays two versions of a line links its subtitle to both.
|
||||
- **Menu text** files are plain UTF-16 text: no size limit; only the edited values change.
|
||||
- Both use the originals store like sounds: texts are recorded as strings (`texts` in store.json) and every
|
||||
file is rebuilt from its original copy + the applied edits (sound and text edits combine in one package).
|
||||
@@ -103,10 +112,18 @@ copy per voice language) is filtered the same way, so the sound list never mixes
|
||||
- Shipped CSVs per archive (names, categories, characters, original size + checksum) make loading instant and
|
||||
enable change detection; any other build is scanned at runtime (header reads only, cached).
|
||||
- Verified speakers come only from the user's lists; name-based character inference is kept in a separate field.
|
||||
- Speech sounds are named after the game's line (speech event), not after their position in the WAD entry:
|
||||
the WEMs of an entry are stored in media-ID order and every language has its own media IDs, so position i
|
||||
is a different line in each language (75 % of the multi-variation lines). Line names make names, speaker
|
||||
labels, mod packages and subtitles agree across languages.
|
||||
- Voice lines are identified by the game's own data, not by names or positions (`docs/formats/banks.md`,
|
||||
*Lines*): the recordings that are sources of the same Wwise Sound objects are one line, with exactly one
|
||||
recording per language (12,434 lines, all 5 languages). The catalog stores that identity (`line`), and
|
||||
speaker labels, mod packages (another language: the same `line`) and subtitles follow it. Positions are
|
||||
useless across languages (WEMs are stored in media-ID order; position i is another line in 75 % of the
|
||||
multi-variation entries).
|
||||
- A line has one name in every language: the most specific speech event that plays it (10,156 of 10,815 voice
|
||||
lines), numbered `NAME#2` in the event's own order when it plays two versions; the other 659 keep the name of
|
||||
their English (INT) recording's position. The position in each language stays an alias (`NAME#i`).
|
||||
- A recording stored twice in a WAD (5 per language) is written and reverted in both places.
|
||||
- The catalog records the ORIGINAL bytes: `tools/build_catalog.py` stops before writing anything if a sound
|
||||
differs from the current catalog (a modified game), unless `--accept-changes` (a game update).
|
||||
- Category scheme: `docs/categories.md`.
|
||||
|
||||
## 6. Repository cleanup choices
|
||||
@@ -123,7 +140,9 @@ copy per voice language) is filtered the same way, so the sound list never mixes
|
||||
store does not need it.
|
||||
|
||||
## 7. Editor UX choices
|
||||
- One window: filters (left), sound table (top), editor of the selected sound (bottom). The primary flow
|
||||
- One window with three tabs. *Sounds*: filters (left) and sound table on top, the editor of the selected
|
||||
sound below across the full width, in three resizable columns (Listen | Replace | Subtitle) so nothing has
|
||||
to scroll at usual window sizes. *Subtitles* and *Menu text*: search, filters, table, editor. The primary flow
|
||||
(search -> select -> Space to listen -> Load file/Record -> Apply) needs no menus; power features (tags,
|
||||
favorites, multi-select revert, mod packages, settings) are in the toolbar and context menu.
|
||||
- Long work (loading, encoding, writing 0.5 GB WADs) runs in background jobs with status messages; the
|
||||
@@ -143,3 +162,18 @@ copy per voice language) is filtered the same way, so the sound list never mixes
|
||||
replacement file, decoded off the GUI thread), next to the Apply/Revert buttons while encoding/writing, and in
|
||||
the status bar for game loading and bulk changes.
|
||||
All filter selectors (including Character) are plain, non-editable drop-downs so they look and behave alike.
|
||||
|
||||
## 8. Verification
|
||||
- `pytest`: unit tests, a synthetic game (WADs, packs, packages with banks, localized banks, dialogue and subtitle
|
||||
objects, localization files), a two-language game built like the real one (per-language BKPK tables, a media
|
||||
ID shared by both languages, a recording stored twice), offscreen GUI tests, and read-only real-game tests
|
||||
when the game is installed.
|
||||
- `ackit verify` (WEMs rebuilt byte-for-byte, encode/decode), `ackit verify --catalog` (catalog vs game files,
|
||||
and every relation re-derived from the game files with independent checks; it prints them as facts).
|
||||
- Real-game write round trips without launching the game: `tools/e2e_features.py [--lang all]` (every copy of
|
||||
an in-memory sound, localized line, prefetch heads, subtitles incl. RSubtitle, menu text, sound + subtitle in
|
||||
one package, a mod moved to another voice language; `all` = each of the 11 languages) and
|
||||
`tools/e2e_editor.py` (through the editor's code path).
|
||||
- In the running game: `ackit verify --ingame` (music heard), `--banks` (patched packages loaded, probe data in
|
||||
memory), `--texts` (menu texts and Startup subtitles found in memory). Every tool reverts in a `finally` block
|
||||
and checks that every touched file is byte-identical afterwards.
|
||||
|
||||
Reference in New Issue
Block a user