Initial commit
This commit is contained in:
6
.gitattributes
vendored
Normal file
6
.gitattributes
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
* text=auto eol=lf
|
||||
*.bat text eol=crlf
|
||||
*.cmd text eol=crlf
|
||||
*.bin binary
|
||||
*.png binary
|
||||
*.ico binary
|
||||
28
.gitignore
vendored
Normal file
28
.gitignore
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
# Python / build outputs
|
||||
__pycache__/
|
||||
.venv/
|
||||
*.egg-info/
|
||||
.pytest_cache/
|
||||
build/
|
||||
dist/
|
||||
|
||||
# local only: third-party tools and reference repos; ackit verify --ingame logs
|
||||
external/
|
||||
logs/
|
||||
|
||||
# Arkham City artwork imported by tools/make_assets.py (Warner Bros.; kept local, bundled in your builds)
|
||||
app/assets/
|
||||
|
||||
# game data, audio and dumps: never commit Arkham City files
|
||||
*.wad
|
||||
*.wem
|
||||
*.bnk
|
||||
*.upk
|
||||
*.dmp
|
||||
*.wav
|
||||
*.ogg
|
||||
*.mp3
|
||||
*.flac
|
||||
*.bin
|
||||
!ackit/data/*.bin
|
||||
*.acmod
|
||||
16
CLAUDE.md
Normal file
16
CLAUDE.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# CLAUDE.md
|
||||
|
||||
Batman: Arkham City (PC) audio toolkit + editor. Read `NOTES.md` first (status, next steps, environment).
|
||||
|
||||
- Layout: `ackit/` library + CLI (`python -m ackit -h`), `app/` PySide6 editor, `tests/` pytest, `data/catalog/`
|
||||
CSVs (+ user's original lists in `data/catalog/source/`, never edit them), `docs/`, `tools/` build scripts.
|
||||
- Dev env: `.venv` (Python 3.14, `pip install -e .[app,dev]`). Run tests with `.venv/Scripts/python -m pytest -q`.
|
||||
- Writing game files goes through `ackit.workspace.Workspace` only (originals store, atomic writes, conflict checks).
|
||||
Speech/ambience: in place, never larger than the original WEM; music packs: any size (`docs/formats/wad.md`).
|
||||
- Never use Steam "verify integrity". Keep game files original after tests (`ackit status`, `ackit revert --all`).
|
||||
- Real-game checks: `ackit verify` (offline), `ackit verify --ingame` (Windows; needs Steam running and logged in,
|
||||
WinDbg; ~1.5 min; reverts and checks byte-identity itself).
|
||||
- Rebuild catalogs only from original game files: `python tools/build_catalog.py`.
|
||||
- Shell: git at `C:\Program Files\Git\cmd` (prepend to PATH). Write Python with the Write/Edit tools; avoid
|
||||
PowerShell here-strings and heredoc string surgery with backslash escapes (they produced NUL bytes before).
|
||||
- The speaker loopback can contain other apps' audio (voice chat): never save raw captures.
|
||||
81
NOTES.md
Normal file
81
NOTES.md
Normal file
@@ -0,0 +1,81 @@
|
||||
# NOTES — Arkham City audio toolkit & editor
|
||||
|
||||
## Current status / Next steps (rewrite as you go)
|
||||
- Mission 1 (2026-10-07): DONE. Format reversed, encoder + injector, verified in-game (no exe patch).
|
||||
- Mission 2 Part 1 (2026-10-08): repository cleanup DONE — tag `pre-cleanup` holds the old state.
|
||||
New layout (README "Where things are"); `ackit` refactored: portable encoder (libsndfile, no ffmpeg/vgmstream),
|
||||
`Game` (Steam/Linux/Proton discovery), catalog from `data/catalog/*.csv` (user's character lists + rules),
|
||||
originals store + `Workspace` (apply/revert/conflicts), mod packages, CLI. 63 tests pass; `ackit verify` PASS;
|
||||
`ackit verify --ingame` PASS (E14: ncc 1.0, revert byte-identical).
|
||||
- Part 2 (2026-10-08): Unheard City DONE (app/, PySide6): browser (search + group/category/character/
|
||||
archive/tag/modified/favorites filters, sortable, multi-select, notes/tags/favorites), editor (original/installed/
|
||||
replacement waveforms, A/B, loop, volume, file or recording, trim/gain/normalize/match loudness, apply/revert,
|
||||
conflict + changed-underneath handling), mod export/import, settings. 72 tests incl. offscreen GUI tests.
|
||||
Packaging: `python tools/build_app.py` -> dist/UnheardCity (self-contained, licenses); packaged exe passes
|
||||
`--smoke-test` with PATH = C:\Windows only. Real e2e through the editor code path (tools/e2e_editor.py): PASS
|
||||
(music heard in game ncc 1.0, reverts byte-identical, speech line apply/revert byte-identical).
|
||||
- NEXT (optional): run the Linux build on a real Linux box/CI (procedure in docs/building.md, workflow in
|
||||
.github/workflows/build.yml); verify >2-channel channel order in game; speech heard in-game (needs gameplay).
|
||||
- OPEN: channel order for >2 channels unverified; Linux package not executed here (no WSL/Docker).
|
||||
|
||||
## Environment facts
|
||||
- Game: Steam AppID 200260 "Batman: Arkham City GOTY", `C:\Program Files (x86)\Steam\steamapps\common\Batman Arkham
|
||||
City GOTY`, exe `Binaries\Win32\BatmanAC.exe` (32-bit, SteamStub). buildid 3718300. Game language: Italian
|
||||
(`Documents\WB Games\...\UserEngine.ini` is UTF-16, `Language=Ita`).
|
||||
- Steam must be running and logged in for the in-game test (`steam.exe -silent`, wait for
|
||||
HKCU\Software\Valve\Steam\ActiveProcess ActiveUser != 0). appmanifest AutoUpdateBehavior=0 (no updates expected).
|
||||
- History: the ITA WAD in the game folder was once the user's modded copy; the clean original came from
|
||||
`%USERPROFILE%\Desktop\ArkhamCityModding\Audio\49D84887_LOC_ITA\WAD\` (sha1 1238172C...). The game is clean now
|
||||
(fingerprints match the old `_backup/`, deleted on 2026-10-08).
|
||||
- Dev tools: `.venv` (Python 3.14.6, PySide6 6.10.3, soundfile 0.14.0 / libsndfile 1.2.2, soxr 1.1.0, numpy,
|
||||
pytest, PyInstaller 6.22.3, soundcard 0.4.6). git at `C:\Program Files\Git\cmd`. WinDbg Store (path changes on
|
||||
update; `ackit/verify/debugger.py: find_cdb`). No WSL distro, no Docker.
|
||||
- User data (this PC): `%APPDATA%\UnheardCity\` (settings.json, stores\<id>\), cache `%LOCALAPPDATA%\UnheardCity\cache`.
|
||||
- The user's own material: `%USERPROFILE%\Desktop\ArkhamCityModding` (old mod work, Ghidra project, dump);
|
||||
their third-party downloads were moved from `tools/`, `repositories/` to `external/` (git-ignored).
|
||||
- Ghidra project BAC: `.text` Ghidra address = runtime + 0xC00 (docs use runtime addresses).
|
||||
|
||||
## Where to look
|
||||
- Findings, failed hypotheses, in-game experiment log E0-E14: `docs/reverse-engineering/findings.md`.
|
||||
- Formats: `docs/formats/`. Decoder addresses: `docs/reverse-engineering/decoder.md`.
|
||||
- Decisions (stack, deps, licenses, originals store, size rule): `docs/decisions.md`.
|
||||
|
||||
## Session log
|
||||
- 2026-10-08: user added `my_catalog_for_ita/` (per-character lists, wemscan names) -> moved to
|
||||
`data/catalog/source/ita/`; 3364 names labelled (2 wemscan name collisions label both candidates; one line had a
|
||||
`.wem.wav` typo). Built catalogs for ITA/INT/FRA/DEU/ESN/SFX (identical names and counts across languages).
|
||||
Assumption: speakers are the same in every language, so ITA labels apply to all.
|
||||
- 2026-10-08: libsndfile's Vorbis writer crashed (C stack overflow) on a 30 s x 6 ch single write -> block writes.
|
||||
- 2026-10-08: wad_fingerprint made structural (IDs + non-pack sizes) so music edits don't invalidate the catalog;
|
||||
scan mode never borrows checksums from the shipped CSV (other builds / synthetic tests).
|
||||
- 2026-10-08: editor async loading looked stuck: Job objects were garbage-collected before their queued result
|
||||
signal arrived -> Runner keeps references until done. Qt 6.10 deprecates invalidateFilter/invalidateRowsFilter ->
|
||||
SoundFilter.refilter uses begin/endFilterChange when available.
|
||||
- 2026-10-08: search haystack must not contain the variation index (":2" matched "2"); IDs are matched separately
|
||||
(words of >= 6 chars against the target id).
|
||||
- Verification commands: `pytest -q`; `ackit verify`; `ackit verify --ingame`; `python tools/e2e_editor.py`
|
||||
(needs Steam logged in); packaged: `dist/UnheardCity/UnheardCity.exe --smoke-test --report f.txt`.
|
||||
- 2026-10-08: UI restyle on request: vector icon set (app/icons.py, no emoji), "Arkham" theme (app/theme.py: QSS +
|
||||
palette, translucent panels over a wallpaper backdrop).
|
||||
- 2026-10-08: user rejected the procedural icon/backdrop and supplied images (~/Downloads/Arkham City:
|
||||
bac_logo_full.png, wallpaper.jpg). tools/make_assets.py builds git-ignored app/assets/ (icon = logo bat over a
|
||||
red waveform; wallpaper copied); app/art.py, make_icon.py and the old icon files removed. Settings ->
|
||||
Background toggle (background_enabled). Character filter is now a plain combobox like the others.
|
||||
- 2026-10-08: theme selector and custom background image removed (one look); the wallpaper is painted only in the
|
||||
filter panel (theme.Sidebar: scaled to panel height, left part shown).
|
||||
- 2026-10-08: build simplified to one offline command: build_app.py runs make_assets when app/assets is missing
|
||||
(--art FOLDER), and license texts live in tools/licenses/ (www.gnu.org was down; no more downloads).
|
||||
- 2026-10-08: neutral dark theme (no blue), editor header icon removed, new icon: flat BAC bat with echolocation
|
||||
arcs. build_app.py also regenerates app/assets when tools/make_assets.py is newer than the icon.
|
||||
- 2026-10-08: background strength default 10 %; obsolete settings keys (theme, background_image) dropped on load.
|
||||
Spinners (app/spinner.py): waveform overlay while loading, editor action row, status bar for global jobs.
|
||||
Replacement files now decode in a background job.
|
||||
- 2026-10-08: accent is now dark red (#b0242c; icons.ACCENT / theme.ACCENT, was AMBER). Icon: white rounded tile,
|
||||
red waveform bars, black logo bat on top. Filter panel opens at its maximum width (320 px, can only shrink).
|
||||
Sidebar veil alpha follows the background strength (at 10 % the wallpaper stays visible).
|
||||
- 2026-10-09: editor renamed "Unheard City" (exe/zip UnheardCity, launcher unheard-city, user data %APPDATA%\UnheardCity).
|
||||
Icon: 9 wider/taller bars (5 at <=24 px), bat 78% of the tile. Panel background #0c0c0c.
|
||||
- 2026-10-09: editor panel opens fitted to its contents (MainWindow.fit_editor; refits on layout changes until
|
||||
the divider is dragged). Windows installer: tools/installer (Install.cmd -> install.ps1/uninstall.ps1, per-user,
|
||||
HKCU uninstall entry), copied into the build by build_app.py; install/upgrade/uninstall cycle verified.
|
||||
User chose no new software (no Inno Setup).
|
||||
55
README.md
Normal file
55
README.md
Normal file
@@ -0,0 +1,55 @@
|
||||
# Arkham City Audio Toolkit
|
||||
|
||||
Replace any voice line, music track or ambience in **Batman: Arkham City (PC, Steam)** with your own audio:
|
||||
by name, from a file or a recording, with one-click revert. No Wwise, no game patch, no external programs.
|
||||
|
||||
- **Unheard City** (`app/`, [user guide](docs/user-guide.md)): the desktop editor for Windows and Linux. Browse and
|
||||
search all ~11,500 sounds, filter by category and character, listen, replace or record, apply and revert.
|
||||
- **`ackit`** (`ackit/`): the Python library and command-line tool behind it.
|
||||
|
||||
How it works in one paragraph: the game's Wwise 2011.1 decoder only knows the standard Vorbis codebooks, so ackit
|
||||
encodes with libvorbis and rewrites the stream into exactly the layout the game expects (codebook IDs, packet
|
||||
format, seek table, header hash and memory size reversed from the game), then writes it into the `.wad`
|
||||
archives atomically, keeping the original bytes in a store for exact reverts. Details in `docs/`.
|
||||
|
||||
## Install
|
||||
- **Editor (users), Windows:** unzip `UnheardCity-<version>-windows-amd64.zip` and double-click **Install.cmd**.
|
||||
It installs for your user (no admin rights), adds a Start menu shortcut and an entry in Settings > Apps for
|
||||
uninstalling. Or skip installing and run `UnheardCity.exe` straight from the unzipped folder.
|
||||
- **Editor (users), Linux:** unpack the `.tar.gz` and run `UnheardCity`.
|
||||
- Nothing else is needed (no Python, no Wwise, no ffmpeg). To build it yourself: [docs/building.md](docs/building.md).
|
||||
- **Toolkit (developers):** Python 3.10+:
|
||||
```
|
||||
python -m venv .venv && .venv/Scripts/activate # Linux: source .venv/bin/activate
|
||||
pip install -e .[app,dev] # app = editor, dev = tests/packaging
|
||||
python -m app.main # run the editor from source
|
||||
```
|
||||
|
||||
## Quick start (command line)
|
||||
```
|
||||
ackit find joker tape # search by name, category or character
|
||||
ackit extract Speech_TAPE_JOKER_1_4 orig.wav # listen to the original
|
||||
ackit replace Speech_TAPE_JOKER_1_4 my_line.wav --match-loudness
|
||||
ackit revert Speech_TAPE_JOKER_1_4 # or: ackit revert --all
|
||||
```
|
||||
More: `ackit status`, `ackit list --character Joker`, `ackit export-mod my.acmod` / `ackit import-mod`,
|
||||
`ackit verify` (offline self-check), `ackit game` (detected install, language, store). `ackit -h` lists all
|
||||
commands. The game folder is found through Steam (Windows, Linux, Flatpak, Proton); otherwise pass `--game DIR`.
|
||||
|
||||
Rules worth knowing: speech/effects replacements must fit in the original's size (the tool squeezes or refuses
|
||||
with the maximum length); music can be any length. Close the game before writing.
|
||||
|
||||
## Where things are
|
||||
```
|
||||
ackit/ library + CLI (formats, encoder, injector, catalog, originals store, workspace)
|
||||
ackit/verify/ offline self-check, in-game test harness (Windows, WinDbg)
|
||||
app/ Unheard City (PySide6)
|
||||
tests/ pytest suite (synthetic game data; real-game tests run when the game is installed)
|
||||
data/catalog/ sound catalogs (CSV), character lists (source/), schema in data/catalog/README.md
|
||||
docs/ formats, reverse engineering, decisions, categories, user guide, building
|
||||
tools/ catalog builder, packaging (build_app.py, installer/), artwork and license scripts
|
||||
NOTES.md project status and working notes; CLAUDE.md: guidance for AI sessions
|
||||
```
|
||||
|
||||
Tests: `pytest` (the real-game tests are skipped when the game is not installed).
|
||||
In-game test (Windows, Steam running, WinDbg installed): `ackit verify --ingame`.
|
||||
39
REPORT.md
Normal file
39
REPORT.md
Normal file
@@ -0,0 +1,39 @@
|
||||
# Report — repository cleanup and the editor, now Unheard City (mission 2)
|
||||
|
||||
## What works (verified)
|
||||
- **Repository**: clean layout (`ackit/`, `app/`, `tests/`, `data/catalog/`, `docs/`, `tools/`); old state in git
|
||||
tag `pre-cleanup`. One CLI (`ackit`) and one app (`UnheardCity`).
|
||||
- **Toolkit**: portable encoder (libvorbis via libsndfile; no Wwise, ffmpeg or vgmstream), WAD/pack injector,
|
||||
originals store with byte-exact revert, conflict detection, mod packages, Steam/Linux/Proton game discovery.
|
||||
- **Catalog**: CSVs for ITA/INT/FRA/DEU/ESN/SFX (11,467 sounds per speech language set), categories, verified
|
||||
speakers from the user's lists (3,366 lines in every language), inferred characters for the rest, original
|
||||
sizes and checksums. Schema in `data/catalog/README.md`.
|
||||
- **Editor**: browse/search/filter, listen (A/B, loop, waveforms), replace from file or recording, trim/gain/
|
||||
normalize/loudness, apply/revert/revert all, changed-underneath warnings, notes/tags/favorites, mod
|
||||
export/import, settings.
|
||||
- **Tests**: 75 automated tests (synthetic game, real-game read-only tests, offscreen GUI tests) pass.
|
||||
`ackit verify` (offline self-check on the installed game) PASS.
|
||||
- **Real game**: `ackit verify --ingame` PASS (ncc 1.0); `tools/e2e_editor.py` — replacement applied through the
|
||||
editor is heard in the game (ncc 1.0, no crash), revert leaves WAD and pack index byte-identical; a speech line
|
||||
applied and reverted byte-identically. Game files are original at the end.
|
||||
- **Package (Windows x86-64)**: `dist/UnheardCity-2.0.0-windows-amd64.zip` (78 MB) with
|
||||
`THIRD_PARTY_LICENSES.txt`; runs its smoke test and starts the GUI with `PATH` reduced to `C:\Windows`.
|
||||
Contains a per-user installer (`Install.cmd`): install, upgrade and uninstall (Settings > Apps) verified.
|
||||
|
||||
## What is not verified / limitations
|
||||
- **Linux package not executed**: no Linux machine, WSL distribution or container runtime was available. The code
|
||||
is portable (no Windows-only calls outside the developer-only in-game harness), Linux/Proton discovery is unit
|
||||
tested with a simulated home folder, and the build procedure + CI workflow are provided
|
||||
(`docs/building.md`, `.github/workflows/build.yml`).
|
||||
- Speech lines being *heard* in the game needs gameplay; verified indirectly (same decoder/WAD path as the music
|
||||
test, byte-level apply/revert, boot test with modified speech WAD).
|
||||
- Speech/effects replacements must fit the original's size (index lives in compressed `.upk` packages).
|
||||
- Channel order for 4-6 channel sounds is unverified (tests used identical channels).
|
||||
- Inputs: WAV/FLAC/OGG/OPUS/MP3/AIFF built in; M4A/AAC/WMA only with an ffmpeg installed on the system.
|
||||
- Character labels outside the user's lists are inferred from names (marked as such), not verified speakers.
|
||||
|
||||
## Next steps
|
||||
1. Run the CI workflow (or the Linux steps in `docs/building.md`) on Ubuntu 22.04 and test the tarball on a Steam
|
||||
Deck/desktop with the game under Proton.
|
||||
2. Extend the curated speaker lists (`data/catalog/source/<lang>/*.txt`) and rebuild with `tools/build_catalog.py`.
|
||||
3. Optional: rewrite the BKPK indexes inside `.upk` packages (LZO) to lift the size limit for speech.
|
||||
10
ackit/__init__.py
Normal file
10
ackit/__init__.py
Normal file
@@ -0,0 +1,10 @@
|
||||
"""ackit - Batman: Arkham City (PC) audio toolkit.
|
||||
|
||||
Library layers (low -> high):
|
||||
formats wad, pack, wem, ogg, bits, codebooks, wwvorbis, allocsize, builder, convert, known_setups
|
||||
audio audio (load/convert/decode), encoder (PCM -> game WEM)
|
||||
game paths, game (install discovery), catalog + classify (names/categories/characters),
|
||||
inject (WAD writing), originals (store), workspace (apply/revert), modpack (share)
|
||||
tools cli (`ackit` command), verify/ (offline self-check, in-game test on Windows)
|
||||
"""
|
||||
__version__ = "2.0.0"
|
||||
3
ackit/__main__.py
Normal file
3
ackit/__main__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from .cli import main
|
||||
|
||||
main()
|
||||
161
ackit/allocsize.py
Normal file
161
ackit/allocsize.py
Normal file
@@ -0,0 +1,161 @@
|
||||
"""Model of the game's codec-setup bump-allocator usage (dwDecodeAllocSize).
|
||||
|
||||
Reversed from the dumped BatmanAC.exe (see docs/decoder_re.md):
|
||||
WwiseVorbisParseSetupHeader (runtime 0x010B4D10) and its callees are a Tremor-lowmem
|
||||
derivative; every pool allocation is rounded up to 4 bytes.
|
||||
"""
|
||||
from .bits import BitReader, ilog, book_maptype1_quantvals
|
||||
from .codebooks import CodebookLibrary
|
||||
|
||||
|
||||
def a4(n):
|
||||
return (n + 3) & ~3
|
||||
|
||||
|
||||
def _node_bytes(used, leafwidth):
|
||||
if used < 2:
|
||||
return 4
|
||||
if leafwidth == 3:
|
||||
leafwidth = 4
|
||||
if ilog(3 * used - 6) + 1 <= leafwidth * 4:
|
||||
return leafwidth // 2 if leafwidth // 2 else 1
|
||||
return leafwidth
|
||||
|
||||
|
||||
def _leaf_words(nodeb, leafwidth):
|
||||
return 2 if leafwidth > nodeb else 1
|
||||
|
||||
|
||||
def _decode_table(used, nodeb, leafw):
|
||||
if nodeb == 4:
|
||||
return used * 8 + 4
|
||||
return a4((used * (leafw + 1) - 2) * nodeb)
|
||||
|
||||
|
||||
def codebook_alloc(blob: bytes) -> int:
|
||||
"""Pool bytes used when unpacking one compact Wwise codebook."""
|
||||
r = BitReader(blob)
|
||||
dims = r.get(4)
|
||||
entries = r.get(14)
|
||||
if r.get(1): # ordered
|
||||
r.get(5)
|
||||
cur = 0
|
||||
while cur < entries:
|
||||
cur += r.get(ilog(entries - cur))
|
||||
used = entries
|
||||
else:
|
||||
cwll = r.get(3)
|
||||
sparse = r.get(1)
|
||||
used = 0
|
||||
for _ in range(entries):
|
||||
if not sparse or r.get(1):
|
||||
r.get(cwll)
|
||||
used += 1
|
||||
maptype = r.get(1)
|
||||
if maptype == 0:
|
||||
lw = ilog(entries) // 8 + 1
|
||||
nodeb = _node_bytes(used, lw)
|
||||
return _decode_table(used, nodeb, _leaf_words(nodeb, lw))
|
||||
r.get(32); r.get(32)
|
||||
qbits = r.get(4) + 1
|
||||
quantvals = book_maptype1_quantvals(entries, dims)
|
||||
total1 = (qbits * dims + 8) // 8
|
||||
total2 = (ilog(quantvals - 1) * dims + 8) // 8 + (qbits + 7) // 8
|
||||
if total1 <= 4 and total1 <= total2:
|
||||
nodeb = _node_bytes(used, total1)
|
||||
return _decode_table(used, nodeb, _leaf_words(nodeb, total1))
|
||||
qv = a4(quantvals) if qbits <= 8 else a4(quantvals * 2)
|
||||
lw = (ilog(quantvals - 1) * dims + 8) // 8
|
||||
nodeb = _node_bytes(used, lw)
|
||||
return qv + _decode_table(used, nodeb, _leaf_words(nodeb, lw))
|
||||
|
||||
|
||||
def setup_alloc(ww_setup: bytes, channels: int, lib: CodebookLibrary) -> dict:
|
||||
"""Walk a stripped Wwise setup and sum pool allocations, by section."""
|
||||
r = BitReader(ww_setup)
|
||||
out = {}
|
||||
nb = r.get(8) + 1
|
||||
total = nb * 0x3C
|
||||
cb = 0
|
||||
for _ in range(nb):
|
||||
cb += codebook_alloc(lib.books[r.get(10)])
|
||||
out["codebooks"] = total + cb
|
||||
nf = r.get(6) + 1
|
||||
fl = nf * 0x24
|
||||
for _ in range(nf):
|
||||
parts = r.get(5)
|
||||
classes = [r.get(4) for _ in range(parts)]
|
||||
maxc = max(classes) if classes else -1
|
||||
dims = []
|
||||
for _ in range(maxc + 1):
|
||||
dims.append(r.get(3) + 1)
|
||||
sub = r.get(2)
|
||||
if sub:
|
||||
r.get(8)
|
||||
for _ in range(1 << sub):
|
||||
r.get(8)
|
||||
r.get(2)
|
||||
rb = r.get(4)
|
||||
posts = sum(dims[c] for c in classes)
|
||||
r.pos += posts * rb
|
||||
fl += a4(parts) + a4(11 * (maxc + 1)) + a4(2 * (posts + 2)) + a4(posts + 2) + 2 * a4(posts)
|
||||
out["floors"] = fl
|
||||
floors_count = nf
|
||||
nr = r.get(6) + 1
|
||||
rs = nr * 0x1C
|
||||
for _ in range(nr):
|
||||
r.get(2); r.get(24); r.get(24); r.get(24)
|
||||
ncls = r.get(6) + 1
|
||||
r.get(8)
|
||||
casc = []
|
||||
for _ in range(ncls):
|
||||
lo = r.get(3)
|
||||
hi = r.get(5) if r.get(1) else 0
|
||||
casc.append(hi * 8 + lo)
|
||||
for c in casc:
|
||||
r.pos += 8 * bin(c & 0xFF).count("1")
|
||||
rs += a4(ncls) + 8 * ncls
|
||||
out["residues"] = rs
|
||||
nm = r.get(6) + 1
|
||||
ms = nm * 0x14
|
||||
for _ in range(nm):
|
||||
submaps = r.get(4) + 1 if r.get(1) else 1
|
||||
if r.get(1):
|
||||
steps = r.get(8) + 1
|
||||
b = ilog(channels - 1)
|
||||
r.pos += steps * 2 * b
|
||||
ms += a4(2 * steps)
|
||||
r.get(2)
|
||||
if submaps > 1:
|
||||
r.pos += 4 * channels
|
||||
ms += a4(channels)
|
||||
r.pos += 24 * submaps
|
||||
ms += a4(2 * submaps)
|
||||
out["mappings"] = ms
|
||||
nmodes = r.get(6) + 1
|
||||
out["modes"] = a4(2 * nmodes)
|
||||
out["total"] = sum(out.values())
|
||||
out["floors_count"] = floors_count
|
||||
return out
|
||||
|
||||
|
||||
def fnv1_32(b: bytes) -> int:
|
||||
"""uHashCodebook: FNV-1 32-bit over the stripped setup packet (verified on all 24 game setups)."""
|
||||
h = 0x811C9DC5
|
||||
for x in b:
|
||||
h = (h * 0x01000193) & 0xFFFFFFFF
|
||||
h ^= x
|
||||
return h
|
||||
|
||||
|
||||
def decode_alloc(ww_setup: bytes, channels: int, lib: CodebookLibrary):
|
||||
"""(dwDecodeAllocSize, dwDecodeX64AllocSize) as the 2011.1 encoder writes them.
|
||||
|
||||
32-bit: 0x30 (setup info) + parser allocations + 4*nbooks + 8 -> exact for all 24 game setups.
|
||||
x64: fitted (alloc + 8*nbooks + 40 + 52*nfloors); not read by the 32-bit game.
|
||||
"""
|
||||
a = setup_alloc(ww_setup, channels, lib)
|
||||
nbooks = ww_setup[0] + 1
|
||||
alloc = 0x30 + a["total"] + 4 * nbooks + 8
|
||||
nfloors = a["floors_count"]
|
||||
return alloc, alloc + 8 * nbooks + 40 + 52 * nfloors
|
||||
136
ackit/audio.py
Normal file
136
ackit/audio.py
Normal file
@@ -0,0 +1,136 @@
|
||||
"""Portable audio helpers (no external programs needed).
|
||||
|
||||
Audio is handled as float32 numpy arrays shaped (frames, channels).
|
||||
Decoding uses libsndfile (WAV, FLAC, OGG/Vorbis/Opus, MP3, AIFF, ...); other formats (M4A, WMA, ...)
|
||||
fall back to an ffmpeg found on the system, if any. Resampling uses soxr.
|
||||
"""
|
||||
import io
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
|
||||
import numpy as np
|
||||
import soundfile as sf
|
||||
|
||||
READABLE = (".wav", ".flac", ".ogg", ".oga", ".opus", ".mp3", ".aif", ".aiff", ".au", ".w64", ".caf")
|
||||
FFMPEG_ONLY = (".m4a", ".aac", ".wma", ".mp4", ".webm", ".mkv", ".ac3")
|
||||
|
||||
|
||||
class AudioError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def _as2d(x):
|
||||
x = np.asarray(x, dtype=np.float32)
|
||||
return x[:, None] if x.ndim == 1 else x
|
||||
|
||||
|
||||
def load(path):
|
||||
"""Read an audio file -> (float32 array (n, ch), rate)."""
|
||||
try:
|
||||
x, sr = sf.read(path, dtype="float32", always_2d=True)
|
||||
return x, sr
|
||||
except (sf.LibsndfileError, RuntimeError, TypeError) as e:
|
||||
ff = shutil.which("ffmpeg")
|
||||
if not ff:
|
||||
raise AudioError("Cannot read '%s' (%s). Supported: WAV, FLAC, OGG, OPUS, MP3, AIFF. "
|
||||
"Convert the file to one of these, or install ffmpeg for other formats."
|
||||
% (os.path.basename(path), e))
|
||||
raw = subprocess.run([ff, "-v", "error", "-i", path, "-f", "wav", "-"], capture_output=True)
|
||||
if raw.returncode:
|
||||
raise AudioError("ffmpeg could not read '%s': %s" % (path, raw.stderr.decode(errors="replace")[-300:]))
|
||||
x, sr = sf.read(io.BytesIO(raw.stdout), dtype="float32", always_2d=True)
|
||||
return x, sr
|
||||
|
||||
|
||||
def save_wav(path, x, rate):
|
||||
sf.write(path, _as2d(x), rate, subtype="PCM_16")
|
||||
|
||||
|
||||
def resample(x, rate, new_rate):
|
||||
x = _as2d(x)
|
||||
if rate == new_rate or len(x) == 0:
|
||||
return x
|
||||
import soxr
|
||||
return soxr.resample(x, rate, new_rate, quality="HQ").astype(np.float32)
|
||||
|
||||
|
||||
def to_channels(x, channels):
|
||||
"""Up/down-mix. Mono is copied to every channel; extra channels get the average."""
|
||||
x = _as2d(x)
|
||||
cur = x.shape[1]
|
||||
if cur == channels:
|
||||
return x
|
||||
if channels == 1:
|
||||
return x.mean(axis=1, keepdims=True)
|
||||
if cur == 1:
|
||||
return np.repeat(x, channels, axis=1)
|
||||
out = np.empty((len(x), channels), dtype=np.float32)
|
||||
keep = min(cur, channels)
|
||||
out[:, :keep] = x[:, :keep]
|
||||
if channels > keep:
|
||||
out[:, keep:] = x.mean(axis=1, keepdims=True)
|
||||
return out
|
||||
|
||||
|
||||
def trim(x, rate, start=None, end=None):
|
||||
x = _as2d(x)
|
||||
a = int(round((start or 0) * rate))
|
||||
b = len(x) if end is None else int(round(end * rate))
|
||||
return x[max(0, a):max(a, min(len(x), b))]
|
||||
|
||||
|
||||
def gain(x, db):
|
||||
return (_as2d(x) * (10 ** (db / 20.0))).astype(np.float32)
|
||||
|
||||
|
||||
def peak_db(x):
|
||||
p = float(np.max(np.abs(x))) if len(x) else 0.0
|
||||
return -120.0 if p <= 0 else 20 * np.log10(p)
|
||||
|
||||
|
||||
def rms_db(x):
|
||||
r = float(np.sqrt(np.mean(np.square(x)))) if len(x) else 0.0
|
||||
return -120.0 if r <= 0 else 20 * np.log10(r)
|
||||
|
||||
|
||||
def normalize(x, peak=-1.0):
|
||||
"""Scale so the highest peak is `peak` dBFS."""
|
||||
p = peak_db(x)
|
||||
return _as2d(x) if p <= -119 else gain(x, peak - p)
|
||||
|
||||
|
||||
def match_loudness(x, reference):
|
||||
"""Scale x to the RMS loudness of `reference` (limited so it does not clip)."""
|
||||
g = rms_db(reference) - rms_db(x)
|
||||
g = min(g, -0.3 - peak_db(x))
|
||||
return gain(x, g)
|
||||
|
||||
|
||||
def trim_silence(x, threshold_db=-50.0, pad=0.02, rate=48000):
|
||||
x = _as2d(x)
|
||||
lvl = np.max(np.abs(x), axis=1)
|
||||
idx = np.nonzero(lvl > 10 ** (threshold_db / 20.0))[0]
|
||||
if len(idx) == 0:
|
||||
return x
|
||||
p = int(pad * rate)
|
||||
return x[max(0, idx[0] - p):min(len(x), idx[-1] + p)]
|
||||
|
||||
|
||||
def conform(x, rate, new_rate, channels):
|
||||
"""Resample + channel conversion + clip guard, ready for encoding."""
|
||||
y = to_channels(resample(x, rate, new_rate), channels)
|
||||
return np.clip(y, -1.0, 1.0)
|
||||
|
||||
|
||||
def decode_wem(data: bytes):
|
||||
"""Decode a game WEM -> (float32 array (n, ch), rate), using the game's codebooks."""
|
||||
from .convert import wem_to_ogg
|
||||
from .wem import Wem
|
||||
ogg, _ = wem_to_ogg(Wem.parse(data))
|
||||
x, sr = sf.read(io.BytesIO(ogg), dtype="float32", always_2d=True)
|
||||
return x, sr
|
||||
|
||||
|
||||
def duration(x, rate):
|
||||
return len(x) / float(rate) if rate else 0.0
|
||||
67
ackit/bits.py
Normal file
67
ackit/bits.py
Normal file
@@ -0,0 +1,67 @@
|
||||
"""LSB-first bit reader/writer (Vorbis bit packing order)."""
|
||||
|
||||
|
||||
class BitReader:
|
||||
def __init__(self, data: bytes, bitpos: int = 0):
|
||||
self.data = data
|
||||
self.pos = bitpos
|
||||
|
||||
def get(self, n: int) -> int:
|
||||
v = 0
|
||||
for i in range(n):
|
||||
byte = self.data[(self.pos >> 3)] if (self.pos >> 3) < len(self.data) else 0
|
||||
v |= ((byte >> (self.pos & 7)) & 1) << i
|
||||
self.pos += 1
|
||||
return v
|
||||
|
||||
def bits_left(self) -> int:
|
||||
return len(self.data) * 8 - self.pos
|
||||
|
||||
|
||||
class BitWriter:
|
||||
def __init__(self):
|
||||
self.buf = bytearray()
|
||||
self.pos = 0
|
||||
|
||||
def put(self, n: int, v: int):
|
||||
for i in range(n):
|
||||
if (self.pos >> 3) >= len(self.buf):
|
||||
self.buf.append(0)
|
||||
if (v >> i) & 1:
|
||||
self.buf[self.pos >> 3] |= 1 << (self.pos & 7)
|
||||
self.pos += 1
|
||||
|
||||
def put_bits_from(self, r: BitReader, n: int):
|
||||
"""Copy n bits from a reader (fast path in 32-bit chunks)."""
|
||||
while n > 0:
|
||||
k = 32 if n >= 32 else n
|
||||
self.put(k, r.get(k))
|
||||
n -= k
|
||||
|
||||
def bytes(self) -> bytes:
|
||||
return bytes(self.buf)
|
||||
|
||||
|
||||
def ilog(v: int) -> int:
|
||||
r = 0
|
||||
while v:
|
||||
r += 1
|
||||
v >>= 1
|
||||
return r
|
||||
|
||||
|
||||
def book_maptype1_quantvals(entries: int, dims: int) -> int:
|
||||
bits = ilog(entries)
|
||||
vals = entries >> ((bits - 1) * (dims - 1) // dims)
|
||||
while True:
|
||||
acc = 1
|
||||
acc1 = 1
|
||||
for _ in range(dims):
|
||||
acc *= vals
|
||||
acc1 *= vals + 1
|
||||
if acc <= entries < acc1:
|
||||
return vals
|
||||
if acc > entries:
|
||||
vals -= 1
|
||||
else:
|
||||
vals += 1
|
||||
128
ackit/builder.py
Normal file
128
ackit/builder.py
Normal file
@@ -0,0 +1,128 @@
|
||||
"""Build an Arkham City compatible WEM from a standard Ogg Vorbis stream.
|
||||
|
||||
Field rules were derived from all 2467 WEMs of the ITA WAD and are validated by
|
||||
rebuilding original files byte-for-byte (scripts/validate_builder.py).
|
||||
"""
|
||||
import struct
|
||||
|
||||
from .ogg import read_packets
|
||||
from .wem import Wem, Chunk
|
||||
from .wwvorbis import V2W, transcode_setup, vorbis_packet_to_ww
|
||||
from .convert import packet_samples, library
|
||||
from .known_setups import lookup_setup
|
||||
from .allocsize import fnv1_32, decode_alloc
|
||||
|
||||
SEEK_INTERVAL = 8192
|
||||
|
||||
|
||||
def build_seek_table(pkt_offsets, pkt_end_granules, interval=SEEK_INTERVAL):
|
||||
"""pkt_offsets: offset of each audio packet relative to the setup start (= post seek-table)."""
|
||||
out = []
|
||||
prev_g, prev_o = 0, 0
|
||||
target = interval
|
||||
for off, g in zip(pkt_offsets, pkt_end_granules):
|
||||
if g >= target:
|
||||
out.append((g - prev_g, off - prev_o))
|
||||
prev_g, prev_o = g, off
|
||||
target = g + interval
|
||||
return out
|
||||
|
||||
|
||||
def build_wem(channels, rate, bs0_exp, bs1_exp, ww_setup, ww_packets, blockflags, total_frames,
|
||||
codebook_hash, decode_alloc, decode_alloc_x64, channel_mask, seek=True, seek_interval=SEEK_INTERVAL, valid_bits=0x18,
|
||||
extra_chunks=()):
|
||||
samples = packet_samples(blockflags, 1 << bs0_exp, 1 << bs1_exp)
|
||||
cum = []
|
||||
g = 0
|
||||
for s in samples:
|
||||
g += s
|
||||
cum.append(g)
|
||||
last_extra = g - total_frames
|
||||
if not 0 <= last_extra < (1 << bs1_exp):
|
||||
raise ValueError("inconsistent total_frames (extra=%d)" % last_extra)
|
||||
|
||||
setup_blob = struct.pack("<H", len(ww_setup)) + ww_setup
|
||||
offs = []
|
||||
pos = len(setup_blob)
|
||||
audio = bytearray()
|
||||
for p in ww_packets:
|
||||
if len(p) > 0xFFFF:
|
||||
raise ValueError("packet too large")
|
||||
offs.append(pos)
|
||||
audio += struct.pack("<H", len(p)) + p
|
||||
pos += 2 + len(p)
|
||||
seek_entries = build_seek_table(offs, cum, seek_interval) if seek else []
|
||||
for dg, do in seek_entries:
|
||||
if dg > 0xFFFF or do > 0xFFFF:
|
||||
raise ValueError("seek delta overflow")
|
||||
seek_blob = b"".join(struct.pack("<HH", a, b) for a, b in seek_entries)
|
||||
data = seek_blob + setup_blob + bytes(audio)
|
||||
st = len(seek_blob)
|
||||
|
||||
w = Wem([Chunk(b"fmt ", b"")] + list(extra_chunks) + [Chunk(b"data", data)])
|
||||
w.channels = channels
|
||||
w.rate = rate
|
||||
w.avg_bps = (len(data) - st) * rate // total_frames
|
||||
w.valid_bits = valid_bits
|
||||
w.channel_mask = channel_mask
|
||||
w.vorb = dict(total_frames=total_frames,
|
||||
loop_start_pkt=len(setup_blob),
|
||||
loop_end_pkt=len(data) - st,
|
||||
loop_begin_extra=0,
|
||||
loop_end_extra=last_extra,
|
||||
seek_table_size=st,
|
||||
vorbis_data_offset=st + len(setup_blob),
|
||||
max_packet_size=max(len(p) for p in ww_packets),
|
||||
last_granule_extra=last_extra,
|
||||
decode_alloc=decode_alloc,
|
||||
decode_alloc_x64=decode_alloc_x64,
|
||||
codebook_hash=codebook_hash,
|
||||
bs0_exp=bs0_exp, bs1_exp=bs1_exp)
|
||||
return w.to_bytes()
|
||||
|
||||
|
||||
DEFAULT_MASKS = {1: 0x4, 2: 0x3, 4: 0x33, 5: 0x37, 6: 0x3F}
|
||||
|
||||
|
||||
def ogg_to_wem(ogg: bytes, total_frames=None, allow_unknown_setup=True):
|
||||
"""Convert a libvorbis Ogg stream into a game-compatible WEM (bytes)."""
|
||||
lib = library()
|
||||
pk = read_packets(ogg)
|
||||
idh = pk[0][0]
|
||||
if idh[:7] != b"\x01vorbis":
|
||||
raise ValueError("not Ogg Vorbis")
|
||||
channels = idh[11]
|
||||
rate = struct.unpack_from("<I", idh, 12)[0]
|
||||
bs0_exp, bs1_exp = idh[28] & 15, idh[28] >> 4
|
||||
ww_setup, info = transcode_setup(pk[2][0], V2W, channels, lib)
|
||||
ww_packets, flags = [], []
|
||||
last_gran = None
|
||||
for p, g in pk[3:]:
|
||||
if not p:
|
||||
continue
|
||||
wp, bf = vorbis_packet_to_ww(p, info, bs0_exp != bs1_exp)
|
||||
ww_packets.append(wp)
|
||||
flags.append(bf)
|
||||
if g is not None:
|
||||
last_gran = g
|
||||
if total_frames is None:
|
||||
total_frames = last_gran
|
||||
hdr = setup_header_values(ww_setup, channels, allow_unknown_setup)
|
||||
return build_wem(channels, rate, bs0_exp, bs1_exp, ww_setup, ww_packets, flags, total_frames,
|
||||
hdr["hash"], hdr["alloc"], hdr["alloc64"],
|
||||
DEFAULT_MASKS.get(channels, (1 << channels) - 1))
|
||||
|
||||
|
||||
def setup_header_values(ww_setup: bytes, channels: int, allow_unknown_setup=True) -> dict:
|
||||
"""hash/alloc values for a setup: from the game's own files when known, else computed
|
||||
with the reversed formulas (identical results on all known setups)."""
|
||||
known = lookup_setup(ww_setup)
|
||||
h = fnv1_32(ww_setup)
|
||||
alloc, alloc64 = decode_alloc(ww_setup, channels, library())
|
||||
if known is not None:
|
||||
if (known["hash"], known["alloc"]) != (h, alloc):
|
||||
raise AssertionError("formula mismatch on known setup")
|
||||
return dict(hash=known["hash"], alloc=known["alloc"], alloc64=known["alloc64"], known=True)
|
||||
if not allow_unknown_setup:
|
||||
raise ValueError("setup not used by any game file")
|
||||
return dict(hash=h, alloc=alloc, alloc64=alloc64, known=False)
|
||||
262
ackit/catalog.py
Normal file
262
ackit/catalog.py
Normal file
@@ -0,0 +1,262 @@
|
||||
"""Index of every sound in the game, with names, categories and characters.
|
||||
|
||||
Sources, in order:
|
||||
1. data/catalog/<ARCHIVE>.csv -- shipped catalog (instant) when the WAD fingerprint matches;
|
||||
2. otherwise a header scan of the WAD (a few seconds, cached in the user cache), classified with
|
||||
`classify` and labelled with data/catalog/characters.csv (speaker per sound name).
|
||||
Names follow the community hash list (ackit/data/hashlist.txt): an entry with one WEM is NAME
|
||||
(alias NAME_1), entries with n variations are NAME_1..NAME_n (alias NAME#i); stream-pack media are
|
||||
named by their ID or <Pack>_<ID>.
|
||||
"""
|
||||
import csv
|
||||
import difflib
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import struct
|
||||
from dataclasses import dataclass, asdict
|
||||
|
||||
from . import classify
|
||||
from .game import wad_fingerprint
|
||||
from .inject import pack_bnk_path
|
||||
from .pack import Pack
|
||||
from .paths import catalog_dir, package_data, user_dir
|
||||
from .wad import Wad
|
||||
|
||||
FIELDS = ["archive", "target", "name", "aliases", "group", "category", "character",
|
||||
"characters_inferred", "seconds", "rate", "channels", "size", "sha1", "notes"]
|
||||
SCAN_VERSION = 1
|
||||
|
||||
|
||||
@dataclass
|
||||
class Sound:
|
||||
archive: str
|
||||
target: str
|
||||
name: str
|
||||
aliases: tuple = ()
|
||||
group: str = "Unknown"
|
||||
category: str = "Unknown"
|
||||
character: str = "" # verified speaker(s), '; '-separated (curated catalog)
|
||||
characters_inferred: str = "" # characters/roles mentioned by the name (hint)
|
||||
seconds: float = 0.0
|
||||
rate: int = 0
|
||||
channels: int = 0
|
||||
size: int = 0 # size of the ORIGINAL WEM (bytes) when known from the catalog
|
||||
sha1: str = "" # first 16 hex digits of the original WEM's SHA-1 ('' if unknown)
|
||||
notes: str = ""
|
||||
|
||||
@property
|
||||
def key(self):
|
||||
return "%s:%s" % (self.archive, self.target)
|
||||
|
||||
@property
|
||||
def is_pack_media(self):
|
||||
return "/" in self.target
|
||||
|
||||
@property
|
||||
def limit(self):
|
||||
"""Max replacement size in bytes (None = any size)."""
|
||||
return None if self.is_pack_media else self.size
|
||||
|
||||
def names(self):
|
||||
return (self.name,) + tuple(self.aliases)
|
||||
|
||||
|
||||
# --- static data ---------------------------------------------------------------------------
|
||||
_names = None
|
||||
|
||||
|
||||
def hash_names():
|
||||
global _names
|
||||
if _names is None:
|
||||
_names = {}
|
||||
for ln in open(package_data("hashlist.txt"), encoding="latin-1"):
|
||||
p = ln.rstrip("\r\n").split("\t")
|
||||
if len(p) >= 2 and p[1]:
|
||||
try:
|
||||
_names[int(p[0], 16)] = p[1]
|
||||
except ValueError:
|
||||
pass
|
||||
return _names
|
||||
|
||||
|
||||
def load_characters(path=None):
|
||||
"""{sound name (lower): 'Speaker' or 'A; B'} from data/catalog/characters.csv."""
|
||||
path = path or os.path.join(catalog_dir(), "characters.csv")
|
||||
out = {}
|
||||
if os.path.exists(path):
|
||||
for row in csv.DictReader(open(path, encoding="utf-8")):
|
||||
out[row["name"].lower()] = row["character"]
|
||||
return out
|
||||
|
||||
|
||||
def read_csv(path):
|
||||
out = []
|
||||
for r in csv.DictReader(open(path, encoding="utf-8", newline="")):
|
||||
out.append(Sound(archive=r["archive"], target=r["target"], name=r["name"],
|
||||
aliases=tuple(a for a in r["aliases"].split("|") if a), group=r["group"],
|
||||
category=r["category"], character=r["character"],
|
||||
characters_inferred=r["characters_inferred"], seconds=float(r["seconds"] or 0),
|
||||
rate=int(r["rate"] or 0), channels=int(r["channels"] or 0), size=int(r["size"] or 0),
|
||||
sha1=r["sha1"], notes=r["notes"]))
|
||||
return out
|
||||
|
||||
|
||||
def write_csv(path, sounds):
|
||||
with open(path, "w", encoding="utf-8", newline="") as f:
|
||||
w = csv.DictWriter(f, FIELDS, lineterminator="\n")
|
||||
w.writeheader()
|
||||
for s in sounds:
|
||||
d = asdict(s)
|
||||
d["aliases"] = "|".join(s.aliases)
|
||||
d["seconds"] = "%.3f" % s.seconds
|
||||
w.writerow({k: d[k] for k in FIELDS})
|
||||
|
||||
|
||||
# --- scanning ------------------------------------------------------------------------------
|
||||
def _hdr(b):
|
||||
"""(rate, channels, seconds) from the first bytes of a WEM (fmt is always the first chunk)."""
|
||||
if len(b) >= 0x30 and b[:4] == b"RIFF" and b[12:16] == b"fmt ":
|
||||
ch, rate = struct.unpack_from("<HI", b, 22)
|
||||
frames = struct.unpack_from("<I", b, 44)[0]
|
||||
return rate, ch, (frames / float(rate) if rate else 0.0)
|
||||
return 0, 0, 0.0
|
||||
|
||||
|
||||
def _walk_entry(f, offset, size):
|
||||
"""Offsets/lengths of the WEMs inside one WAD entry, reading only headers."""
|
||||
out = []
|
||||
pos = 0
|
||||
while pos + 12 <= size:
|
||||
f.seek(offset + pos)
|
||||
h = f.read(0x30)
|
||||
if h[:4] != b"RIFF" or h[8:12] != b"WAVE":
|
||||
break
|
||||
ln = 8 + struct.unpack_from("<I", h, 4)[0]
|
||||
out.append((pos, ln, h))
|
||||
# next WEM starts at the first non-zero byte after this one (zero padding in between)
|
||||
cand = pos + ln
|
||||
f.seek(offset + cand)
|
||||
nxt = None
|
||||
while cand < size and nxt is None:
|
||||
chunk = f.read(min(0x10000, size - cand))
|
||||
if not chunk:
|
||||
break
|
||||
stripped = chunk.lstrip(b"\0")
|
||||
if stripped:
|
||||
nxt = cand + len(chunk) - len(stripped)
|
||||
cand += len(chunk)
|
||||
if nxt is None:
|
||||
break
|
||||
pos = nxt
|
||||
return out
|
||||
|
||||
|
||||
def scan_archive(archive, wad_path, characters=None, progress=None):
|
||||
"""Header scan of one WAD -> [Sound]."""
|
||||
names = hash_names()
|
||||
characters = characters if characters is not None else load_characters()
|
||||
wad = Wad(wad_path)
|
||||
out = []
|
||||
with open(wad_path, "rb") as f:
|
||||
for n, e in enumerate(wad.entries):
|
||||
if progress and n % 200 == 0:
|
||||
progress(n, len(wad.entries))
|
||||
bnk = pack_bnk_path(wad_path, e.hash)
|
||||
if bnk and open(bnk, "rb").read(4) == b"BKPK":
|
||||
pk = Pack(open(bnk, "rb").read())
|
||||
pname = names.get(e.hash, "%08X" % e.hash)
|
||||
for mid, size, off in pk.entries:
|
||||
f.seek(e.offset + off)
|
||||
rate, ch, secs = _hdr(f.read(0x30))
|
||||
out.append(Sound(archive, "%08X/%08X" % (e.hash, mid), names.get(mid, "%s_%08X" % (pname, mid)),
|
||||
seconds=secs, rate=rate, channels=ch, size=size))
|
||||
out[-1].group, out[-1].category = classify.classify(out[-1].name, pack=pname)
|
||||
continue
|
||||
wems = _walk_entry(f, e.offset, e.size)
|
||||
base = names.get(e.hash, "%08X" % e.hash)
|
||||
for i, (off, ln, h) in enumerate(wems):
|
||||
rate, ch, secs = _hdr(h)
|
||||
if len(wems) == 1:
|
||||
s = Sound(archive, "%08X" % e.hash, base, (base + "_1",))
|
||||
else:
|
||||
s = Sound(archive, "%08X:%d" % (e.hash, i), "%s_%d" % (base, i + 1), ("%s#%d" % (base, i + 1),))
|
||||
s.seconds, s.rate, s.channels, s.size = secs, rate, ch, ln
|
||||
s.group, s.category = classify.classify(s.name)
|
||||
out.append(s)
|
||||
for s in out:
|
||||
s.characters_inferred = "; ".join(classify.infer_characters(s.name))
|
||||
s.character = next((characters[a.lower()] for a in s.names() if a.lower() in characters), "")
|
||||
return out
|
||||
|
||||
|
||||
# --- catalog -------------------------------------------------------------------------------
|
||||
def _shipped_index():
|
||||
p = os.path.join(catalog_dir(), "index.json")
|
||||
try:
|
||||
return json.load(open(p))
|
||||
except (OSError, ValueError):
|
||||
return {}
|
||||
|
||||
|
||||
def load_archive(game, archive, progress=None):
|
||||
"""[Sound] for one archive of a game (shipped CSV, user-cache scan, or fresh scan)."""
|
||||
path = game.wad(archive)
|
||||
fp = wad_fingerprint(path)
|
||||
shipped = _shipped_index().get(archive, {})
|
||||
csv_path = os.path.join(catalog_dir(), "%s.csv" % archive)
|
||||
if shipped.get("fingerprint") == fp and os.path.exists(csv_path):
|
||||
return read_csv(csv_path)
|
||||
cache = os.path.join(user_dir("cache"), "scan_%s_%s_v%d.csv" % (archive, fp, SCAN_VERSION))
|
||||
if os.path.exists(cache):
|
||||
return read_csv(cache)
|
||||
sounds = scan_archive(archive, path, progress=progress)
|
||||
# unknown build: no original checksums (states then rely on the originals store only)
|
||||
write_csv(cache, sounds)
|
||||
return sounds
|
||||
|
||||
|
||||
class Catalog:
|
||||
"""All sounds of the selected archives (default: the game's speech language + SFX)."""
|
||||
|
||||
def __init__(self, game, archives=None, progress=None):
|
||||
self.game = game
|
||||
self.archives = archives or [a for a in (game.language(), "SFX") if a in game.archives]
|
||||
self.sounds = []
|
||||
for a in self.archives:
|
||||
self.sounds += load_archive(game, a, progress)
|
||||
self.by_key = {s.key: s for s in self.sounds}
|
||||
self._by_name = {}
|
||||
for s in self.sounds:
|
||||
for n in s.names():
|
||||
self._by_name.setdefault(n.lower(), []).append(s)
|
||||
|
||||
def get(self, key):
|
||||
return self.by_key[key]
|
||||
|
||||
def resolve(self, name):
|
||||
"""One sound by name, alias, key ('ITA:B49CDF48:11') or file name ('X_3.wav')."""
|
||||
if name in self.by_key:
|
||||
return self.by_key[name]
|
||||
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:
|
||||
return uniq[0]
|
||||
if len(uniq) > 1:
|
||||
raise LookupError("'%s' is ambiguous; use one of: %s" % (name, ", ".join(
|
||||
next((a for a in s.names() if a.lower() != key), s.key) for s in uniq)))
|
||||
near = [s.name for s in self.search(key, limit=8)] or self.suggest(key)
|
||||
raise LookupError("no sound named '%s'.%s" % (name, ("\n did you mean: " + ", ".join(near)) if near else ""))
|
||||
|
||||
def search(self, text, limit=None):
|
||||
"""All words must appear in the name, category or character (case-insensitive)."""
|
||||
words = [w for w in re.split(r"[\s_]+", text.lower()) if w]
|
||||
res = [s for s in self.sounds
|
||||
if all(w in s.name.lower() or w in s.category.lower() or w in s.character.lower() for w in words)]
|
||||
res.sort(key=lambda s: (len(s.name), s.name))
|
||||
return res[:limit] if limit else res
|
||||
|
||||
def suggest(self, text, n=8):
|
||||
names = {s.name.lower(): s.name for s in self.sounds}
|
||||
return [names[k] for k in difflib.get_close_matches(text.lower(), list(names), n=n, cutoff=0.6)]
|
||||
107
ackit/classify.py
Normal file
107
ackit/classify.py
Normal file
@@ -0,0 +1,107 @@
|
||||
"""Classify sounds into group / category and infer characters from Wwise names.
|
||||
|
||||
Scheme (documented in docs/categories.md):
|
||||
group Speech | Music | Ambience | SFX | Unknown
|
||||
category finer bucket derived from the name prefix (e.g. "Interview tapes", "Combat barks")
|
||||
Characters: the authoritative speaker comes from the curated catalog (data/catalog/characters.csv,
|
||||
built from the user's per-character lists). For other lines `infer_characters` returns the
|
||||
characters/roles that the *name* mentions -- a hint, not a speaker label (conversations alternate
|
||||
speakers between variations).
|
||||
"""
|
||||
import re
|
||||
|
||||
# (regex on the upper-case name, group, category) -- first match wins
|
||||
_RULES = [
|
||||
(r"^(CHR_RADIOSCANNER|PSO_CARRADIO|SPEECH_RADIO|SPEECH_TV|SPEECH_CAMERAS|DIA_)", "Speech", "Radio & TV"),
|
||||
(r"^SPEECH_TAPE_", "Speech", "Interview tapes"),
|
||||
(r"^SPEECH_INTERROGATION_", "Speech", "Interrogations"),
|
||||
(r"^SPEECH_(CINEMATIC|NEW)_", "Speech", "Cinematics"),
|
||||
(r"^SPEECH_BOSS_", "Speech", "Boss fights"),
|
||||
(r"^SPEECH_RIDDLE(R|S)_", "Speech", "Riddler"),
|
||||
(r"^SPEECH_SIDE_", "Speech", "Side missions"),
|
||||
(r"^SPEECH_DLC_", "Speech", "DLC"),
|
||||
(r"^SPEECH_PRED_", "Speech", "Predator chatter"),
|
||||
(r"^SPEECH_(COMBAT|BULLY)_", "Speech", "Combat barks"),
|
||||
(r"^SPEECH_EMOTE_", "Speech", "Efforts & emotes"),
|
||||
(r"^SPEECH_REACT_", "Speech", "Reactions"),
|
||||
(r"^SPEECH_(STREETS|ZONES|OUTCAST|VERTICAL)_", "Speech", "Open-world chatter"),
|
||||
(r"^SPEECH_CHAR_", "Speech", "Character lines"),
|
||||
(r"^SPEECH_(CH|SEQUENCE|STEEL|MUSEUM|CHURCH|COURT|UNDER|GCPD|ZP1)_", "Speech", "Story missions"),
|
||||
(r"^SPEECH_(GAME|MAP|AUGMENTED|FORENSICS|TEMP)_", "Speech", "Gameplay & hints"),
|
||||
(r"^NPC_", "Ambience", "Crowds"),
|
||||
(r"^SPEECH_", "Speech", "Other speech"),
|
||||
(r"^MUSIC_PDLC", "Music", "Music (DLC)"),
|
||||
(r"^MUSIC", "Music", "Music"),
|
||||
(r"^(AMB|PSA)_", "Ambience", "Ambience"),
|
||||
(r"^WND_", "Ambience", "Wind"),
|
||||
(r"^P?VOL_", "Ambience", "Room tone"),
|
||||
(r"^SEQ_", "SFX", "Scripted sequences"),
|
||||
(r"^OBJ_", "SFX", "Objects"),
|
||||
(r"^WPN_", "SFX", "Weapons"),
|
||||
(r"^CHR_", "SFX", "Character foley"),
|
||||
(r"^GLOBAL", "SFX", "Global"),
|
||||
]
|
||||
_RULES = [(re.compile(p), g, c) for p, g, c in _RULES]
|
||||
|
||||
GROUPS = ("Speech", "Music", "Ambience", "SFX", "Unknown")
|
||||
|
||||
|
||||
def classify(name: str, pack: str = None):
|
||||
"""-> (group, category). `pack` = name of the stream pack a media belongs to, if any."""
|
||||
n = name.upper()
|
||||
if pack:
|
||||
p = pack.upper()
|
||||
if p.startswith("MUSIC"):
|
||||
return "Music", "Music (DLC)" if "PDLC" in p else "Music"
|
||||
for rx, g, c in _RULES:
|
||||
if rx.search(n):
|
||||
return g, c
|
||||
return "Unknown", "Unknown"
|
||||
|
||||
|
||||
# --- characters -------------------------------------------------------------------------------
|
||||
# display names for the labels used in the user's catalog files (data/catalog/source/ita/*.txt)
|
||||
USER_LABELS = {
|
||||
"Alfred": "Alfred", "Azrael": "Azrael", "Bane": "Bane", "Batman": "Batman",
|
||||
"Calendar": "Calendar Man", "Catwoman": "Catwoman", "Croc": "Killer Croc", "Deadshot": "Deadshot",
|
||||
"Freeze": "Mr. Freeze", "Gordon": "Gordon", "Ivy": "Poison Ivy", "Joker": "Joker",
|
||||
"Madhatter": "Mad Hatter", "Oracle": "Oracle", "Penguin": "Penguin", "Quinn": "Harley Quinn",
|
||||
"Ras": "Ra's al Ghul", "Riddler": "Riddler", "Robin": "Robin", "Strange": "Hugo Strange",
|
||||
"Talia": "Talia al Ghul", "Twoface": "Two-Face", "Unknown": "Unknown", "Vicky": "Vicki Vale",
|
||||
"Zsazz": "Victor Zsasz",
|
||||
}
|
||||
|
||||
# name tokens -> character / role (matched on whole "_"-separated tokens, digits ignored)
|
||||
_TOKENS = {
|
||||
"Batman": ("BATMAN", "BM"), "Catwoman": ("CATWOMAN", "CAT", "CW"), "Joker": ("JOKER",),
|
||||
"Penguin": ("PENGUIN", "PENG"), "Two-Face": ("TWOFACE", "TF"), "Riddler": ("RIDDLER",),
|
||||
"Mr. Freeze": ("FREEZE", "MRFREEZE", "FREEZESUIT"), "Poison Ivy": ("IVY",),
|
||||
"Hugo Strange": ("STRANGE", "HUGO"), "Robin": ("ROBIN",), "Oracle": ("ORACLE",),
|
||||
"Alfred": ("ALFRED", "ALF"), "Harley Quinn": ("QUINN", "HARLEY"), "Ra's al Ghul": ("RAS",),
|
||||
"Talia al Ghul": ("TALIA",), "Victor Zsasz": ("ZSASZ", "ZSAZZ"), "Bane": ("BANE",),
|
||||
"Deadshot": ("DEADSHOT",), "Mad Hatter": ("MADHATTER", "HATTER"), "Calendar Man": ("CALENDAR", "CM"),
|
||||
"Hush": ("HUSH",), "Clayface": ("CLAYFACE", "CLAY"), "Solomon Grundy": ("GRUNDY",),
|
||||
"Azrael": ("AZRAEL",), "Vicki Vale": ("VALE", "VICKI", "VICKY", "VIC"), "Gordon": ("GORDON",),
|
||||
"Nightwing": ("NIGHTWING", "NW"), "Killer Croc": ("CROC",),
|
||||
# roles
|
||||
"Thug": ("THUG", "THUGS", "GENTHUG", "JTHUG", "JTHUGS", "PTHUG", "PTHUGS", "BULLY"),
|
||||
"TYGER guard": ("TYGER",), "Ninja": ("NINJA", "ASSASSIN"), "Doctor": ("DOC", "DOCTOR"),
|
||||
"Cop": ("COP", "COPS", "GCPD"), "Political prisoner": ("HOBO", "OUTCAST", "OC"),
|
||||
"Hostage": ("HOST", "HOSTAGE"), "Guard": ("GUARD", "GUARDS"),
|
||||
}
|
||||
_TOK2CHAR = {t: c for c, toks in _TOKENS.items() for t in toks}
|
||||
_STRIP_DIGITS = re.compile(r"\d+$")
|
||||
|
||||
|
||||
def infer_characters(name: str):
|
||||
"""Characters/roles mentioned by a sound name (ordered, unique). Empty if none."""
|
||||
n = name.upper()
|
||||
out = []
|
||||
if n.startswith("SPEECH_TAPE_"):
|
||||
out.append("Hugo Strange") # Strange's interview tapes
|
||||
for tok in re.split(r"[_\s]+", n):
|
||||
t = _STRIP_DIGITS.sub("", tok)
|
||||
c = _TOK2CHAR.get(t) or _TOK2CHAR.get(tok)
|
||||
if c and c not in out:
|
||||
out.append(c)
|
||||
return out
|
||||
335
ackit/cli.py
Normal file
335
ackit/cli.py
Normal file
@@ -0,0 +1,335 @@
|
||||
"""ackit - Batman: Arkham City audio toolkit (command line).
|
||||
|
||||
Everyday use, by sound name (see `ackit find`):
|
||||
ackit find joker tape search names, categories, characters
|
||||
ackit info Speech_TAPE_JOKER_1_4 details, size limit, modified state
|
||||
ackit extract Speech_TAPE_JOKER_1_4 orig.wav save the original (or --current)
|
||||
ackit replace Speech_TAPE_JOKER_1_4 line.wav encode + install a replacement
|
||||
ackit revert Speech_TAPE_JOKER_1_4 put the original back (--all for everything)
|
||||
ackit status what you have changed
|
||||
ackit export-mod my.acmod / import-mod my.acmod share your changes
|
||||
|
||||
The game folder is detected from Steam (Windows, Linux, Proton); override with --game DIR.
|
||||
"""
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
|
||||
from . import audio, settings
|
||||
from .game import Game, GameError, find_installs, default_game_dir
|
||||
|
||||
|
||||
def _workspace(a, archives=None):
|
||||
from .workspace import Workspace
|
||||
st = settings.load()
|
||||
gd = a.game or default_game_dir(st)
|
||||
if not gd:
|
||||
raise GameError("Arkham City was not found. Pass the install folder with --game DIR "
|
||||
"(the folder that contains BmGame and Binaries).")
|
||||
if a.game:
|
||||
st["game_dir"] = os.path.abspath(a.game)
|
||||
settings.save(st)
|
||||
game = Game(gd)
|
||||
if archives is None and getattr(a, "lang", None):
|
||||
archives = [a.lang, "SFX"]
|
||||
return Workspace(game, a.store or st.get("store_root"), archives,
|
||||
full_backup=not getattr(a, "no_full_backup", False) and st.get("full_backup", True),
|
||||
progress=lambda n, t: None)
|
||||
|
||||
|
||||
def _row(s, ws=None):
|
||||
mod = " *" if ws and s.key in ws.store.entries and ws.store.entries[s.key].get("applied") else ""
|
||||
who = s.character or (("~" + s.characters_inferred) if s.characters_inferred else "")
|
||||
return "%-46s %-4s %7.2fs %-20s %s%s" % (s.name, s.archive, s.seconds, s.category[:20], who[:28], mod)
|
||||
|
||||
|
||||
# --- everyday ---------------------------------------------------------------------------------
|
||||
def cmd_find(a):
|
||||
ws = _workspace(a)
|
||||
res = ws.catalog.search(" ".join(a.words))
|
||||
if not res:
|
||||
near = ws.catalog.suggest("_".join(a.words))
|
||||
print("nothing found." + (" Did you mean: " + ", ".join(near) if near else ""))
|
||||
return
|
||||
for s in (res if a.all else res[:40]):
|
||||
print(_row(s, ws))
|
||||
if not a.all and len(res) > 40:
|
||||
print("... %d more (--all)" % (len(res) - 40))
|
||||
|
||||
|
||||
def cmd_list(a):
|
||||
ws = _workspace(a)
|
||||
rows = ws.catalog.sounds
|
||||
if a.group:
|
||||
rows = [s for s in rows if s.group.lower() == a.group.lower()]
|
||||
if a.category:
|
||||
rows = [s for s in rows if a.category.lower() in s.category.lower()]
|
||||
if a.character:
|
||||
c = a.character.lower()
|
||||
rows = [s for s in rows if c in s.character.lower() or (a.inferred and c in s.characters_inferred.lower())]
|
||||
if a.modified:
|
||||
rows = [s for s in rows if s.key in ws.modified_keys()]
|
||||
if a.csv:
|
||||
from .catalog import write_csv
|
||||
write_csv(a.csv, rows)
|
||||
print("wrote %s (%d sounds)" % (a.csv, len(rows)))
|
||||
return
|
||||
for s in rows:
|
||||
print(_row(s, ws))
|
||||
print("%d sound(s)" % len(rows))
|
||||
|
||||
|
||||
def cmd_info(a):
|
||||
if a.name.lower().endswith(".wem") and os.path.exists(a.name):
|
||||
from .wem import Wem
|
||||
m = Wem.parse(open(a.name, "rb").read())
|
||||
print("rate %d, channels %d, mask 0x%x" % (m.rate, m.channels, m.channel_mask))
|
||||
for k, v in m.vorb.items():
|
||||
print(" %-20s %s" % (k, hex(v) if k == "codebook_hash" else v))
|
||||
return
|
||||
ws = _workspace(a)
|
||||
s = ws.sound(a.name)
|
||||
for k in ("name", "key", "group", "category", "character", "characters_inferred", "seconds", "rate",
|
||||
"channels"):
|
||||
print("%-20s %s" % (k, getattr(s, k)))
|
||||
print("%-20s %s" % ("aliases", ", ".join(s.aliases)))
|
||||
lim = ws.limit(s)
|
||||
print("%-20s %s" % ("max size", "any" if lim is None else "%d bytes (the original's size)" % lim))
|
||||
print("%-20s %s" % ("state", ws.state(s)))
|
||||
|
||||
|
||||
def cmd_extract(a):
|
||||
ws = _workspace(a)
|
||||
s = ws.sound(a.name)
|
||||
data = ws.current_bytes(s) if a.current else ws.original_bytes(s)
|
||||
if a.out.lower().endswith(".wem"):
|
||||
open(a.out, "wb").write(data)
|
||||
else:
|
||||
x, sr = audio.decode_wem(data)
|
||||
audio.save_wav(a.out, x, sr)
|
||||
print("wrote %s (%s, %.2f s)" % (a.out, s.name, s.seconds))
|
||||
|
||||
|
||||
def _process(a, x, sr, ws, s):
|
||||
if a.trim:
|
||||
start, _, end = a.trim.partition(":")
|
||||
x = audio.trim(x, sr, float(start or 0), float(end) if end else None)
|
||||
if a.trim_silence:
|
||||
x = audio.trim_silence(x, rate=sr)
|
||||
if a.match_loudness:
|
||||
ref, _ = ws.original_audio(s)
|
||||
x = audio.match_loudness(x, ref)
|
||||
if a.normalize:
|
||||
x = audio.normalize(x)
|
||||
if a.gain:
|
||||
x = audio.gain(x, a.gain)
|
||||
return x
|
||||
|
||||
|
||||
def cmd_replace(a):
|
||||
ws = _workspace(a)
|
||||
s = ws.sound(a.name)
|
||||
x, sr = audio.load(a.audio)
|
||||
x = _process(a, x, sr, ws, s)
|
||||
secs = audio.duration(x, sr)
|
||||
if not s.is_pack_media and secs > s.seconds * 1.15 + 0.25:
|
||||
print("warning: your audio (%.2f s) is longer than the original (%.2f s); it will be squeezed into the "
|
||||
"original's size and the game may cut it off" % (secs, s.seconds))
|
||||
wem, info = ws.prepare(s, x, sr, a.quality, progress=print)
|
||||
ws.apply([(s, wem, {"source": os.path.basename(a.audio)})], force=a.force, progress=print)
|
||||
print("replaced %s (%d Hz, q%.1f, %d bytes). Undo: ackit revert %s" % (s.name, info["rate"], info["quality"],
|
||||
len(wem), s.name))
|
||||
|
||||
|
||||
def cmd_apply(a):
|
||||
ws = _workspace(a)
|
||||
changes, errors = [], []
|
||||
for f in sorted(os.listdir(a.folder)):
|
||||
p = os.path.join(a.folder, f)
|
||||
if not os.path.isfile(p) or f.startswith("."):
|
||||
continue
|
||||
try:
|
||||
s = ws.sound(f)
|
||||
x, sr = audio.load(p)
|
||||
wem, _ = ws.prepare(s, x, sr, a.quality)
|
||||
changes.append((s, wem, {"source": f}))
|
||||
print("ok %-40s -> %s" % (f, s.name))
|
||||
except (LookupError, ValueError, audio.AudioError) as e:
|
||||
errors.append(f)
|
||||
print("skip %-40s %s" % (f, e))
|
||||
if changes:
|
||||
ws.apply(changes, force=a.force)
|
||||
print("%d applied, %d skipped" % (len(changes), len(errors)))
|
||||
|
||||
|
||||
def cmd_revert(a):
|
||||
ws = _workspace(a)
|
||||
if a.all:
|
||||
done = ws.revert_all(progress=print)
|
||||
else:
|
||||
done = ws.revert([ws.sound(n) for n in a.names], progress=print)
|
||||
print("reverted %d sound(s)" % len(done))
|
||||
|
||||
|
||||
def cmd_status(a):
|
||||
ws = _workspace(a)
|
||||
keys = ws.modified_keys()
|
||||
if not keys:
|
||||
print("no sounds modified; game audio is original")
|
||||
return
|
||||
for k in keys:
|
||||
s = ws.catalog.by_key.get(k)
|
||||
st = ws.state(s) if s else "(other language)"
|
||||
print("%-50s %s" % (s.name if s else k, st))
|
||||
|
||||
|
||||
def cmd_export_mod(a):
|
||||
from .modpack import export_mod
|
||||
ws = _workspace(a)
|
||||
keys = [ws.sound(n).key for n in a.names] if a.names else ws.modified_keys()
|
||||
n = export_mod(ws, a.out, keys, a.title or os.path.splitext(os.path.basename(a.out))[0], a.author or "",
|
||||
a.description or "")
|
||||
print("wrote %s (%d sounds)" % (a.out, n))
|
||||
|
||||
|
||||
def cmd_import_mod(a):
|
||||
from .modpack import import_mod
|
||||
ws = _workspace(a)
|
||||
for im in import_mod(ws, a.file, force=a.force, progress=print):
|
||||
print("%-8s %-46s %s" % (im.how, im.item.get("name"), im.note))
|
||||
|
||||
|
||||
# --- advanced ---------------------------------------------------------------------------------
|
||||
def cmd_encode(a):
|
||||
from .encoder import encode
|
||||
if a.like:
|
||||
ws = _workspace(a)
|
||||
s = ws.sound(a.like)
|
||||
rate, ch = s.rate, s.channels
|
||||
else:
|
||||
rate, ch = a.rate, a.channels
|
||||
x, sr = audio.load(a.audio)
|
||||
wem, info = encode(x, sr, rate, ch, a.quality)
|
||||
open(a.out, "wb").write(wem)
|
||||
print("wrote %s (%d Hz, %d ch, q%.1f, %d bytes)" % (a.out, info["rate"], ch, info["quality"], len(wem)))
|
||||
|
||||
|
||||
def cmd_inject(a):
|
||||
ws = _workspace(a)
|
||||
s = ws.sound(a.name)
|
||||
ws.apply([(s, open(a.wem, "rb").read(), {"source": os.path.basename(a.wem)})], force=a.force)
|
||||
print("injected %s" % s.name)
|
||||
|
||||
|
||||
def cmd_decode(a):
|
||||
x, sr = audio.decode_wem(open(a.wem, "rb").read())
|
||||
audio.save_wav(a.out, x, sr)
|
||||
print("wrote %s (%.2f s, %d Hz, %d ch)" % (a.out, len(x) / sr, sr, x.shape[1]))
|
||||
|
||||
|
||||
def cmd_game(a):
|
||||
st = settings.load()
|
||||
print("installs found:", ", ".join(find_installs()) or "none")
|
||||
gd = a.game or default_game_dir(st)
|
||||
if gd:
|
||||
g = Game(gd)
|
||||
from .workspace import default_store_root
|
||||
print("game folder: ", g.root)
|
||||
print("archives: ", ", ".join("%s=%s" % (k, g.rel(v)) for k, v in g.archives.items()))
|
||||
print("speech language: ", g.language())
|
||||
print("originals store: ", a.store or st.get("store_root") or default_store_root(g))
|
||||
print("settings file: ", settings.path())
|
||||
|
||||
|
||||
def cmd_verify(a):
|
||||
from .verify import selfcheck
|
||||
ws = _workspace(a)
|
||||
if not a.ingame:
|
||||
ok = selfcheck.run(ws, log=print)
|
||||
sys.exit(0 if ok else 1)
|
||||
from .verify import ingame
|
||||
if a.baseline:
|
||||
rep = ingame.run(ws, a.target, mode="baseline", seconds=a.seconds)
|
||||
elif a.installed:
|
||||
rep = ingame.run(ws, a.target, mode="installed", seconds=a.seconds)
|
||||
else:
|
||||
s = ws.sound(a.target)
|
||||
x, sr = audio.load(a.audio) if a.audio else ingame.probe_signal()
|
||||
wem, _ = ws.prepare(s, x, sr)
|
||||
rep = ingame.run(ws, a.target, wem=wem, mode="modded", seconds=a.seconds)
|
||||
for k, v in rep.items():
|
||||
print("%-24s %s" % (k, v))
|
||||
sys.exit(0 if rep["result"] == "PASS" else 1)
|
||||
|
||||
|
||||
def build_parser():
|
||||
common = argparse.ArgumentParser(add_help=False)
|
||||
common.add_argument("--game", metavar="DIR", help="Arkham City install folder (remembered)")
|
||||
common.add_argument("--lang", type=str.upper, help="speech language archive, e.g. ITA, INT (default: game setting)")
|
||||
common.add_argument("--store", metavar="DIR", help="originals store folder (default: user data folder)")
|
||||
enc = argparse.ArgumentParser(add_help=False)
|
||||
enc.add_argument("--quality", default="auto", help="auto (default) or libvorbis quality 0..10")
|
||||
enc.add_argument("--force", action="store_true", help="overwrite sounds changed outside this tool")
|
||||
enc.add_argument("--no-full-backup", action="store_true", help="skip the one-time full WAD backup")
|
||||
proc = argparse.ArgumentParser(add_help=False)
|
||||
proc.add_argument("--trim", metavar="START:END", help="keep only this part (seconds), e.g. 0.5:4.2")
|
||||
proc.add_argument("--trim-silence", action="store_true", help="cut leading/trailing silence")
|
||||
proc.add_argument("--gain", type=float, metavar="DB", help="volume change in dB")
|
||||
proc.add_argument("--normalize", action="store_true", help="peak-normalize to -1 dBFS")
|
||||
proc.add_argument("--match-loudness", action="store_true", help="match the original's loudness")
|
||||
|
||||
p = argparse.ArgumentParser(prog="ackit", description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
sp = p.add_subparsers(dest="cmd", required=True, metavar="COMMAND")
|
||||
|
||||
def add(name, f, help, parents=(), **kw):
|
||||
s = sp.add_parser(name, parents=[common] + list(parents), help=help, description=help, **kw)
|
||||
s.set_defaults(f=f)
|
||||
return s
|
||||
|
||||
s = add("find", cmd_find, "search sounds by name, category or character")
|
||||
s.add_argument("words", nargs="+"); s.add_argument("--all", action="store_true")
|
||||
s = add("list", cmd_list, "list sounds with filters (or export them as CSV)")
|
||||
s.add_argument("--group"); s.add_argument("--category"); s.add_argument("--character")
|
||||
s.add_argument("--inferred", action="store_true", help="also match characters inferred from names")
|
||||
s.add_argument("--modified", action="store_true"); s.add_argument("--csv", metavar="FILE")
|
||||
s = add("info", cmd_info, "show details of a sound (or of a .wem file)")
|
||||
s.add_argument("name")
|
||||
s = add("extract", cmd_extract, "save a sound as .wav (decoded) or .wem")
|
||||
s.add_argument("name"); s.add_argument("out"); s.add_argument("--current", action="store_true",
|
||||
help="the installed version instead of the original")
|
||||
s = add("replace", cmd_replace, "replace a sound with an audio file (WAV/FLAC/OGG/MP3/...)", (enc, proc))
|
||||
s.add_argument("name"); s.add_argument("audio")
|
||||
s = add("apply", cmd_apply, "replace every sound named by the files in a folder", (enc,))
|
||||
s.add_argument("folder")
|
||||
s = add("revert", cmd_revert, "put original sounds back", aliases=["restore"])
|
||||
s.add_argument("names", nargs="*"); s.add_argument("--all", action="store_true")
|
||||
add("status", cmd_status, "list the sounds you modified")
|
||||
s = add("export-mod", cmd_export_mod, "save your changes as a shareable .acmod package")
|
||||
s.add_argument("out"); s.add_argument("names", nargs="*", help="default: everything you modified")
|
||||
s.add_argument("--title"); s.add_argument("--author"); s.add_argument("--description")
|
||||
s = add("import-mod", cmd_import_mod, "install a .acmod package", (enc,))
|
||||
s.add_argument("file")
|
||||
add("game", cmd_game, "show the detected game folder, archives, language and store")
|
||||
s = add("verify", cmd_verify, "self-check (offline), or test in the real game with --ingame (Windows)")
|
||||
s.add_argument("--ingame", action="store_true"); s.add_argument("--target", default="SFX:EDF036D6/393BC53A")
|
||||
s.add_argument("--audio"); s.add_argument("--baseline", action="store_true")
|
||||
s.add_argument("--installed", action="store_true"); s.add_argument("--seconds", type=float, default=40)
|
||||
# advanced
|
||||
s = add("encode", cmd_encode, "[advanced] audio -> .wem file", (enc,))
|
||||
s.add_argument("audio"); s.add_argument("out"); s.add_argument("--like", metavar="NAME")
|
||||
s.add_argument("--rate", type=int, default=28000); s.add_argument("--channels", type=int, default=1)
|
||||
s = add("inject", cmd_inject, "[advanced] install a prepared .wem file", (enc,))
|
||||
s.add_argument("name"); s.add_argument("wem")
|
||||
s = add("decode", cmd_decode, "[advanced] .wem file -> .wav")
|
||||
s.add_argument("wem"); s.add_argument("out")
|
||||
return p
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
a = build_parser().parse_args(argv)
|
||||
try:
|
||||
a.f(a)
|
||||
except (GameError, LookupError, ValueError, audio.AudioError) as e:
|
||||
sys.exit("error: %s" % e)
|
||||
except PermissionError as e:
|
||||
sys.exit("error: cannot write %s - close the game and try again" % getattr(e, "filename", "a game file"))
|
||||
118
ackit/codebooks.py
Normal file
118
ackit/codebooks.py
Normal file
@@ -0,0 +1,118 @@
|
||||
"""Wwise packed codebook library (ww2ogg 'packed_codebooks.bin' format).
|
||||
|
||||
The game's built-in table (runtime 0x0158E748, 598 entries) is byte-identical to
|
||||
ww2ogg's standard packed_codebooks.bin; see docs/wem_format.md.
|
||||
"""
|
||||
import os
|
||||
import struct
|
||||
|
||||
from .bits import BitReader, BitWriter, ilog, book_maptype1_quantvals
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
DEFAULT_LIB = os.path.join(HERE, "data", "game_codebooks.bin")
|
||||
|
||||
|
||||
def load_library(path: str = DEFAULT_LIB) -> list:
|
||||
b = open(path, "rb").read()
|
||||
table = struct.unpack_from("<I", b, len(b) - 4)[0]
|
||||
n = (len(b) - table) // 4
|
||||
offs = struct.unpack_from("<%dI" % n, b, table)
|
||||
return [b[offs[i]:offs[i + 1]] for i in range(n - 1)]
|
||||
|
||||
|
||||
def save_library(path: str, books: list):
|
||||
out = bytearray()
|
||||
offs = []
|
||||
for bk in books:
|
||||
offs.append(len(out))
|
||||
out += bk
|
||||
offs.append(len(out)) # table start == end of last book
|
||||
out += struct.pack("<%dI" % len(offs), *offs)
|
||||
open(path, "wb").write(bytes(out))
|
||||
|
||||
|
||||
def wwise_book_to_vorbis(blob: bytes, w: BitWriter):
|
||||
"""Expand a compact Wwise codebook into a standard Vorbis codebook (ww2ogg rebuild)."""
|
||||
r = BitReader(blob)
|
||||
w.put(24, 0x564342)
|
||||
dims = r.get(4); w.put(16, dims)
|
||||
entries = r.get(14); w.put(24, entries)
|
||||
ordered = r.get(1); w.put(1, ordered)
|
||||
if ordered:
|
||||
w.put(5, r.get(5))
|
||||
cur = 0
|
||||
while cur < entries:
|
||||
nb = ilog(entries - cur)
|
||||
num = r.get(nb); w.put(nb, num)
|
||||
cur += num
|
||||
else:
|
||||
cwll = r.get(3)
|
||||
sparse = r.get(1); w.put(1, sparse)
|
||||
if cwll == 0 or cwll > 5:
|
||||
raise ValueError("bad codeword length length")
|
||||
for _ in range(entries):
|
||||
present = 1
|
||||
if sparse:
|
||||
present = r.get(1); w.put(1, present)
|
||||
if present:
|
||||
w.put(5, r.get(cwll))
|
||||
lookup = r.get(1); w.put(4, lookup)
|
||||
if lookup == 1:
|
||||
w.put(32, r.get(32)); w.put(32, r.get(32))
|
||||
vlen = r.get(4); w.put(4, vlen)
|
||||
w.put(1, r.get(1))
|
||||
for _ in range(book_maptype1_quantvals(entries, dims)):
|
||||
w.put(vlen + 1, r.get(vlen + 1))
|
||||
elif lookup != 0:
|
||||
raise ValueError("bad lookup type")
|
||||
return r.pos
|
||||
|
||||
|
||||
def skip_vorbis_book(r: BitReader):
|
||||
"""Advance reader past one standard Vorbis codebook; returns (dims, entries)."""
|
||||
if r.get(24) != 0x564342:
|
||||
raise ValueError("codebook sync lost")
|
||||
dims = r.get(16)
|
||||
entries = r.get(24)
|
||||
if r.get(1): # ordered
|
||||
r.get(5)
|
||||
cur = 0
|
||||
while cur < entries:
|
||||
cur += r.get(ilog(entries - cur))
|
||||
else:
|
||||
sparse = r.get(1)
|
||||
for _ in range(entries):
|
||||
if not sparse or r.get(1):
|
||||
r.get(5)
|
||||
lookup = r.get(4)
|
||||
if lookup in (1, 2):
|
||||
r.get(32); r.get(32)
|
||||
vlen = r.get(4); r.get(1)
|
||||
n = book_maptype1_quantvals(entries, dims) if lookup == 1 else entries * dims
|
||||
r.pos += n * (vlen + 1)
|
||||
elif lookup != 0:
|
||||
raise ValueError("bad lookup type %d" % lookup)
|
||||
return dims, entries
|
||||
|
||||
|
||||
def book_bits_key(data: bytes, start: int, end: int) -> tuple:
|
||||
"""Hashable representation of a bit range [start,end)."""
|
||||
n = end - start
|
||||
v = int.from_bytes(data, "little") >> start
|
||||
return (n, v & ((1 << n) - 1))
|
||||
|
||||
|
||||
class CodebookLibrary:
|
||||
def __init__(self, path: str = DEFAULT_LIB):
|
||||
self.books = load_library(path)
|
||||
self.by_vorbis = {}
|
||||
self.vorbis = []
|
||||
for i, blob in enumerate(self.books):
|
||||
w = BitWriter()
|
||||
wwise_book_to_vorbis(blob, w)
|
||||
key = (w.pos, int.from_bytes(w.bytes(), "little") & ((1 << w.pos) - 1))
|
||||
self.vorbis.append(key)
|
||||
self.by_vorbis.setdefault(key, i)
|
||||
|
||||
def lookup(self, key):
|
||||
return self.by_vorbis.get(key)
|
||||
56
ackit/convert.py
Normal file
56
ackit/convert.py
Normal file
@@ -0,0 +1,56 @@
|
||||
"""WEM <-> Ogg Vorbis conversion using the game's codebook library."""
|
||||
from .codebooks import CodebookLibrary
|
||||
from .ogg import OggWriter
|
||||
from .wem import Wem
|
||||
from .wwvorbis import (W2V, transcode_setup, build_id_header, build_comment_header,
|
||||
ww_packet_to_vorbis)
|
||||
|
||||
_LIB = None
|
||||
|
||||
|
||||
def library() -> CodebookLibrary:
|
||||
global _LIB
|
||||
if _LIB is None:
|
||||
_LIB = CodebookLibrary()
|
||||
return _LIB
|
||||
|
||||
|
||||
def packet_samples(blockflags, bs0, bs1):
|
||||
"""Samples produced by each packet (Vorbis overlap-add rule)."""
|
||||
out = []
|
||||
prev = None
|
||||
for bf in blockflags:
|
||||
cur = bs1 if bf else bs0
|
||||
out.append(0 if prev is None else (prev // 4 + cur // 4))
|
||||
prev = cur
|
||||
return out
|
||||
|
||||
|
||||
def wem_to_ogg(wem: Wem):
|
||||
"""Returns (ogg_bytes, details dict)."""
|
||||
lib = library()
|
||||
v = wem.vorb
|
||||
setup, info = transcode_setup(wem.setup_packet(), W2V, wem.channels, lib)
|
||||
pkts, end = wem.audio_packets()
|
||||
out_pkts = []
|
||||
flags = []
|
||||
prev_bf = 0
|
||||
for i, (_, p) in enumerate(pkts):
|
||||
nxt = pkts[i + 1][1][0] if i + 1 < len(pkts) else None
|
||||
op, bf = ww_packet_to_vorbis(p, nxt, info, prev_bf, v["bs0_exp"] != v["bs1_exp"])
|
||||
out_pkts.append(op)
|
||||
flags.append(bf)
|
||||
prev_bf = bf
|
||||
bs0, bs1 = 1 << v["bs0_exp"], 1 << v["bs1_exp"]
|
||||
samples = packet_samples(flags, bs0, bs1)
|
||||
ow = OggWriter()
|
||||
ow.page([build_id_header(wem.channels, wem.rate, v["bs0_exp"], v["bs1_exp"])], 0, bos=True)
|
||||
ow.page([build_comment_header(), setup], 0)
|
||||
g = 0
|
||||
total = v["total_frames"]
|
||||
for i, op in enumerate(out_pkts):
|
||||
g += samples[i]
|
||||
last = i == len(out_pkts) - 1
|
||||
ow.page([op], min(g, total) if last else g, eos=last)
|
||||
return ow.bytes(), dict(info=info, flags=flags, samples=samples, packets=pkts, data_end=end,
|
||||
sum_samples=sum(samples))
|
||||
BIN
ackit/data/game_codebooks.bin
Normal file
BIN
ackit/data/game_codebooks.bin
Normal file
Binary file not shown.
19063
ackit/data/hashlist.txt
Normal file
19063
ackit/data/hashlist.txt
Normal file
File diff suppressed because it is too large
Load Diff
278
ackit/data/known_setups.json
Normal file
278
ackit/data/known_setups.json
Normal file
@@ -0,0 +1,278 @@
|
||||
[
|
||||
{
|
||||
"setup": "1f5482728054b348c111846215e38891cdcd019f34495c82560edc58311c610c7e714201ef9817e31e41800920304050f08510102382109921120aab608141193438cc03800788088900203141917671015d06b8a08bbb0e8410842004b1388002127070c20d4fbce109373841a7a8d4410000008000000f000007051011d15c85c5054686c606478747000000c003001f0000c7071011d15c85c5054686c6064787470000000000101010001800",
|
||||
"hash": 2329567961,
|
||||
"alloc": 15492,
|
||||
"alloc64": 15892,
|
||||
"count": 8819,
|
||||
"formats": {
|
||||
"28000/1ch/8-11": 8593,
|
||||
"28000/5ch/8-11": 176,
|
||||
"28000/4ch/8-11": 50
|
||||
},
|
||||
"conflict": false
|
||||
},
|
||||
{
|
||||
"setup": "1f5482728054b348c111846215e38891cdcd019f34495c82560edc58311c610c7e714201ef9817e31e41800920304050f08510102382109921120aab608141193438cc03800788088900203141917671815906b8a08bbb0e841084200495378002127070c20d4fbce109373289a6288c410000006000000f000007051011d15c85c5054686c606478747000000e002001f0000c7071011d15c85c5054686c6064787470000000000101010001800",
|
||||
"hash": 1136153196,
|
||||
"alloc": 15492,
|
||||
"alloc64": 15892,
|
||||
"count": 2338,
|
||||
"formats": {
|
||||
"44100/5ch/8-11": 70,
|
||||
"44100/1ch/8-11": 2242,
|
||||
"44100/4ch/8-11": 8,
|
||||
"44100/3ch/8-11": 18
|
||||
},
|
||||
"conflict": false
|
||||
},
|
||||
{
|
||||
"setup": "16e5805702114f621065896afd16605711f468c4647a39075522fc21a90d6020426602810228309001000708095200406181a174a10b4288205d04593c70e1c48d276e38a1430b040000001c00f001007030001111cd35383c4044484c50545800000000000000",
|
||||
"hash": 953446082,
|
||||
"alloc": 13464,
|
||||
"alloc64": 13740,
|
||||
"count": 631,
|
||||
"formats": {
|
||||
"24000/1ch/10-10": 568,
|
||||
"24000/5ch/10-10": 59,
|
||||
"24000/4ch/10-10": 4
|
||||
},
|
||||
"conflict": false
|
||||
},
|
||||
{
|
||||
"setup": "22870c218c290e39866353e5805702114f621065896afd166057e44d970f4a4af9925032e76c31937c35b1e45720202400c00041c10c00303840183910e8082070680d44c84c6050080d0e3201e00122422a00484c50942e74410811a48b208b072e9cb8f1c40d277468830000000002003e0000120a2022229ab90a8b0b8c0c8d0d8e0e8f0f90900000000004003e000012112022229ab90a8b0b8c0c8d0d8e0e8f0f90900000000000202020003000",
|
||||
"hash": 3571053844,
|
||||
"alloc": 17692,
|
||||
"alloc64": 18116,
|
||||
"count": 436,
|
||||
"formats": {
|
||||
"24000/4ch/9-10": 92,
|
||||
"24000/1ch/9-10": 219,
|
||||
"24000/5ch/9-10": 125
|
||||
},
|
||||
"conflict": false
|
||||
},
|
||||
{
|
||||
"setup": "1f5482728054b348c111846215e38891cdcd019f34495c82560edc58311c610c7e714201ef9817e31e41800920304050f08510102382109921120aab608141193438cc03800788088900203141917671015d06b8a08bbb0e8410842004b1388002127070c20d4fbce109373841a7a8d4410000007000000f000007051011d15c85c5054686c6064787470000004003001f0000c7071011d15c85c5054686c6064787470000000000101010001800",
|
||||
"hash": 2903324457,
|
||||
"alloc": 15492,
|
||||
"alloc64": 15892,
|
||||
"count": 71,
|
||||
"formats": {
|
||||
"32000/1ch/8-11": 71
|
||||
},
|
||||
"conflict": false
|
||||
},
|
||||
{
|
||||
"setup": "2251ec90561827a2f14482527004a158c53862647373c0274d1297a095c33f4206918e66427463d11a1956955420202400c00041c10c00303840f81c049d00c1d12208911922d1b0101c1e540244c454009098a0900b00151617691717d065800bbab8eb4008410842108b032820010727dcf0c41b9e708313748a4a1d040000000700f00000705c001111cd6164686c7074787c8084040000003800f001007088001111cd6164686c7074787c80840400000000000101018001",
|
||||
"hash": 3644545348,
|
||||
"alloc": 16192,
|
||||
"alloc64": 16616,
|
||||
"count": 39,
|
||||
"formats": {
|
||||
"32000/4ch/8-11": 38,
|
||||
"32000/5ch/8-11": 1
|
||||
},
|
||||
"conflict": false
|
||||
},
|
||||
{
|
||||
"setup": "2251ec90561827a2f14482527004a158c53862647373c0274d1297a095c33f4206918e66427463d11a1956955420202400c00041c10c00303840f81c049d00c1d12208911922d1b0101c1e540244c454009098a0900b001516176917179865800bbab8eb40084108425079032820010727dcf0c41b9e702393688ac218040000000600f00000705c001111cd6164686c7074787c8084040000002c00f001007088001111cd6164686c7074787c80840400000000000101018001",
|
||||
"hash": 2446091980,
|
||||
"alloc": 16192,
|
||||
"alloc64": 16616,
|
||||
"count": 15,
|
||||
"formats": {
|
||||
"48000/1ch/8-11": 14,
|
||||
"48000/4ch/8-11": 1
|
||||
},
|
||||
"conflict": false
|
||||
},
|
||||
{
|
||||
"setup": "2251ec90561827a2f14482527004a158c53862647373c0274d1297a095831c3130d181385c9472d1816328a14320202400c00041c10c00303840f81c049d00c1d12208911922d1b0101c1e540244c454009098a0900b00151617691717d065800bbab8eb4008410842108b032820010727dcf0c41b9e708313748a4a1d040000000800f00000705c001111cd6164686c7074787c8084040000003c00f001007088001111cd6164686c7074787c80840400000000000101018001",
|
||||
"hash": 1876663221,
|
||||
"alloc": 16872,
|
||||
"alloc64": 17296,
|
||||
"count": 12,
|
||||
"formats": {
|
||||
"32000/4ch/8-11": 12
|
||||
},
|
||||
"conflict": false
|
||||
},
|
||||
{
|
||||
"setup": "0f3244050615634958db0f703c960d8bcfe9d74f3540800920304050300a418030020100008003007c00001c0540444473060708090a0b0c0d0e0f00000000000000",
|
||||
"hash": 3867129568,
|
||||
"alloc": 12084,
|
||||
"alloc64": 12304,
|
||||
"count": 5,
|
||||
"formats": {
|
||||
"6000/1ch/9-9": 5
|
||||
},
|
||||
"conflict": false
|
||||
},
|
||||
{
|
||||
"setup": "1f5482728054b348c111846215e38891cdcd019f34495c82560edc58311c610c7e714201ef9817e31e41800920304050f08510102382109921120aab608141193438cc03800788088900203141917671815906b8a08bbb0e841084200495378002127070c20d4fbce109373289a6288c410000006000000f000007051011d15c85c5054686c606478747000000a002001f0000c7071011d15c85c5054686c6064787470000000000101010001800",
|
||||
"hash": 3242781484,
|
||||
"alloc": 15492,
|
||||
"alloc64": 15892,
|
||||
"count": 5,
|
||||
"formats": {
|
||||
"48000/1ch/8-11": 4,
|
||||
"48000/5ch/8-11": 1
|
||||
},
|
||||
"conflict": false
|
||||
},
|
||||
{
|
||||
"setup": "215482728054b348c111846215e38891cdcd019f34495c82560e5869d2d088b6e0f21f00210e02d372c9a514049800020304055f0801312208911922a1b00a1618944183c33c007880889008001213146917179865800bbab8eb40084108425079032820010727dcf0c41b9e702393688ac218080000000c00f000008050001111cd5c85c5054686c6064787c70788000000c005001f000048081011d1cc55585c6064686c7074787c800402080000800002101010001800",
|
||||
"hash": 3080104147,
|
||||
"alloc": 13300,
|
||||
"alloc64": 13716,
|
||||
"count": 5,
|
||||
"formats": {
|
||||
"44100/2ch/8-11": 5
|
||||
},
|
||||
"conflict": false
|
||||
},
|
||||
{
|
||||
"setup": "269e95744f1611aec48f756c45e64485527004a158c53862647373c0274d1297a09543905f0d164c1f2246879165c751e05d40206402810228309001000708095200406181a143840810a3c0c0b8b8b40842648648442c068909d54051311d002c2e30e4034086c646dac5057419e0822eee3a1042108210c4e2000a48c0c109373cf18627dce0049da252070100000002003c00001c1b404444731c1d1e1f2021222324250100000010007c00001c26404444731c1d1e1f2021222324250100000000404040006000",
|
||||
"hash": 3462290664,
|
||||
"alloc": 18180,
|
||||
"alloc64": 18636,
|
||||
"count": 5,
|
||||
"formats": {
|
||||
"32000/4ch/8-11": 5
|
||||
},
|
||||
"conflict": false
|
||||
},
|
||||
{
|
||||
"setup": "299e95744f1611aec48f756c45e64485527004a158c53862647373c0274d1297a095837f9b68309b42c8d538d406a8e0d7176c79741710089940a0000a0c6400c00142821400505860e8102102c42830302e2e2d8210992112118b4162423550544c07008b0b0cf90090a1b1917671015d06b8a08bbb0e8410842004b1388002127070c20d4fbce109373841a7a8d481000000d000000f0000c906101111cd1c4787c7074888c8084989c9098a0000002006001f0000490a101111cd1c4787c7074888c8084989c9094a20800000000820000101018001",
|
||||
"hash": 73756765,
|
||||
"alloc": 16080,
|
||||
"alloc64": 16560,
|
||||
"count": 5,
|
||||
"formats": {
|
||||
"48000/2ch/8-11": 5
|
||||
},
|
||||
"conflict": false
|
||||
},
|
||||
{
|
||||
"setup": "12324405061563a147550072b5a6da831842219e351646821d106002080c10148c4210208c400000000001001f00004901101111cd9cc1014282c2024383c303448404000000000000",
|
||||
"hash": 1050329265,
|
||||
"alloc": 13788,
|
||||
"alloc64": 14032,
|
||||
"count": 4,
|
||||
"formats": {
|
||||
"12000/1ch/9-9": 4
|
||||
},
|
||||
"conflict": false
|
||||
},
|
||||
{
|
||||
"setup": "215482728054b348c111846215e38891cdcd019f34495c82560e88e0955e5e6f2c894d403bfec14a95e14016049800020304055f0801312208911922a1b00a1618944183c33c00788088900800121314691717d065800bbab8eb4008410842108b032820010727dcf0c41b9e708313748a4a1d080000000d00f000008050001111cd5c85c5054686c6064787c707880000008006001f000048081011d1cc55585c6064686c7074787c800402080000800002101010001800",
|
||||
"hash": 2982013646,
|
||||
"alloc": 15116,
|
||||
"alloc64": 15532,
|
||||
"count": 3,
|
||||
"formats": {
|
||||
"32000/2ch/8-11": 3
|
||||
},
|
||||
"conflict": false
|
||||
},
|
||||
{
|
||||
"setup": "2551ec90561827a2f14482527004a158c53862647373c0274d1297a09503319ce8b498461e9ab89483ec48c0804841fd14080809003040503003000c0e103e0741274070b40842648648342c048707950011311500242628e4024085c545dac5057419e0822eee3a1042108210c4e2000a48c0c109373cf18627dce0049da252070200008003003c00002417404444347318191a1b1c1d1e1f2021222324020000001c007c00002425404444347318191a1b1c1d1e1f20212223248100020000208000040404000600",
|
||||
"hash": 3568494628,
|
||||
"alloc": 14316,
|
||||
"alloc64": 14764,
|
||||
"count": 3,
|
||||
"formats": {
|
||||
"32000/2ch/8-11": 3
|
||||
},
|
||||
"conflict": false
|
||||
},
|
||||
{
|
||||
"setup": "2551ec90561827a2f14482527004a158c53862647373c0274d1297a095432339b5a2d842b8e592da44ebd145a0726f9d15080809003040503003000c0e103e0741274070b40842648648342c048707950011311500242628e4024085c545dac5057419e0822eee3a1042108210c4e2000a48c0c109373cf18627dce0049da25207020000c003003c00002417404444347318191a1b1c1d1e1f2021222324020000001e007c00002425404444347318191a1b1c1d1e1f20212223248100020000208000040404000600",
|
||||
"hash": 1680511206,
|
||||
"alloc": 14540,
|
||||
"alloc64": 14988,
|
||||
"count": 2,
|
||||
"formats": {
|
||||
"32000/2ch/8-11": 2
|
||||
},
|
||||
"conflict": false
|
||||
},
|
||||
{
|
||||
"setup": "16e5805702114f621065896afd16605711f468c4647a39075522fc21a90d6020426602810228309001000708095200406181a174a10b4288205d04593c70e1c48d276e38a1430b040000001a00f001007030001111cd35383c4044484c50545800000000000000",
|
||||
"hash": 3765588556,
|
||||
"alloc": 13464,
|
||||
"alloc64": 13740,
|
||||
"count": 2,
|
||||
"formats": {
|
||||
"24000/1ch/10-10": 2
|
||||
},
|
||||
"conflict": false
|
||||
},
|
||||
{
|
||||
"setup": "21870c218c290e39866353e5805702114f621065896afd166057ee31421b2db01c79d38a0d04928047770918080809003040503003000c0e10460e043a02081c5a0311321318144283834c00788088900a00121314a50b5d104204e922c8e2810b276e3c71c3091dda400000000001800f0080840208888866aec2e20223436383a3c3e303440000000002800f0080240408888866aec2e20223436383a3c3e303241040000000041080808000c000",
|
||||
"hash": 2310130245,
|
||||
"alloc": 13076,
|
||||
"alloc64": 13492,
|
||||
"count": 2,
|
||||
"formats": {
|
||||
"24000/2ch/9-10": 2
|
||||
},
|
||||
"conflict": false
|
||||
},
|
||||
{
|
||||
"setup": "2551ec90561827a2f14482527004a158c53862647373c0274d1297a09503319ce8b498461e9ab89483ec48c0804841fd14080809003040503003000c0e103e0741274070b40842648648342c048707950011311500242628e4024085c545dac5056619e0822eee3a104210821054de000a48c0c109373cf18627dcc8249aa23006020000c002003c00002417404444347318191a1b1c1d1e1f20212223240200000016007c00002425404444347318191a1b1c1d1e1f20212223248100020000208000040404000600",
|
||||
"hash": 1844255845,
|
||||
"alloc": 14316,
|
||||
"alloc64": 14764,
|
||||
"count": 2,
|
||||
"formats": {
|
||||
"48000/2ch/8-11": 2
|
||||
},
|
||||
"conflict": false
|
||||
},
|
||||
{
|
||||
"setup": "0f3244050615634958db0f703c960d8bcfe9d74f3540800920304050300a418030020100000003007c00001c0540444473060708090a0b0c0d0e0f00000000000000",
|
||||
"hash": 1823403872,
|
||||
"alloc": 12084,
|
||||
"alloc64": 12304,
|
||||
"count": 1,
|
||||
"formats": {
|
||||
"11025/1ch/9-9": 1
|
||||
},
|
||||
"conflict": false
|
||||
},
|
||||
{
|
||||
"setup": "215482728054b348c111846215e38891cdcd019f34495c82560e5869d2d088b6e0f21f00210e02d372c9a514049800020304055f0801312208911922a1b00a1618944183c33c00788088900800121314691717d065800bbab8eb4008410842108b032820010727dcf0c41b9e708313748a4a1d080000000d00f000008050001111cd5c85c5054686c6064787c707880000008006001f000048081011d1cc55585c6064686c7074787c800402080000800002101010001800",
|
||||
"hash": 2761019796,
|
||||
"alloc": 13300,
|
||||
"alloc64": 13716,
|
||||
"count": 1,
|
||||
"formats": {
|
||||
"32000/2ch/8-11": 1
|
||||
},
|
||||
"conflict": false
|
||||
},
|
||||
{
|
||||
"setup": "1f0d86028427660d044e695b7d849a08083147e36e1cf620dd3a78947554782d4413926bb93155087741800920304050300a4180302208911922a1b00a1618944183c33c00788088900800121314ad2e5c802e0670812edce580100441100461d1001420010e9ce0064f788327b88113e8284aea200000006000800f00808302888868aec2e20223436383a3c3230000001003800f0080e303888868aec2e20223436383a3c3230000000000080808000c00",
|
||||
"hash": 1422405756,
|
||||
"alloc": 17884,
|
||||
"alloc64": 18284,
|
||||
"count": 1,
|
||||
"formats": {
|
||||
"32000/5ch/9-12": 1
|
||||
},
|
||||
"conflict": false
|
||||
},
|
||||
{
|
||||
"setup": "269e95744f1611aec48f756c45e64485527004a158c53862647373c0274d1297a09543905f0d164c1f2246879165c751e05d40206402810228309001000708095200406181a143840810a3c0c0b8b8b40842648648442c068909d54051311d002c2e30e4034086c646dac5057419e0822eee3a1042108210c4e2000a48c0c109373cf18627dce0049da25207010000c001003c00001c1b404444731c1d1e1f202122232425010000800c007c00001c26404444731c1d1e1f2021222324250100000000404040006000",
|
||||
"hash": 2465811007,
|
||||
"alloc": 18180,
|
||||
"alloc64": 18636,
|
||||
"count": 1,
|
||||
"formats": {
|
||||
"48000/1ch/8-11": 1
|
||||
},
|
||||
"conflict": false
|
||||
}
|
||||
]
|
||||
155
ackit/encoder.py
Normal file
155
ackit/encoder.py
Normal file
@@ -0,0 +1,155 @@
|
||||
"""Encode audio into game-compatible WEMs (portable: libvorbis via libsndfile, no Wwise, no ffmpeg).
|
||||
|
||||
Pipeline: float32 PCM -> libvorbis (Ogg) -> `builder.ogg_to_wem` (Wwise 2011.1 layout, game codebook
|
||||
IDs, FNV-1 hash, decode-alloc size). The quality is chosen so that every codebook libvorbis uses exists
|
||||
in the game's built-in library (docs/formats/wem.md); setups the game itself ships are preferred.
|
||||
"""
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
|
||||
import numpy as np
|
||||
import soundfile as sf
|
||||
|
||||
from . import audio
|
||||
from .builder import ogg_to_wem
|
||||
from .convert import library
|
||||
from .known_setups import lookup_setup
|
||||
from .ogg import read_packets
|
||||
from .paths import user_dir
|
||||
from .wwvorbis import V2W, transcode_setup
|
||||
|
||||
QUALITIES = [q / 2 for q in range(20, -1, -1)] # 10.0 .. 0.0 (libvorbis q; libsndfile cannot do q<0)
|
||||
AUTO_MAX_Q_KNOWN = 6.0 # highest q considered when looking for a setup the game ships
|
||||
AUTO_MAX_Q = 4.0 # otherwise, highest compatible q (game files sit around q3-5)
|
||||
FALLBACK_RATES = (32000, 28000, 24000) # e.g. 6 channels at 44.1/48 kHz
|
||||
FIT_RATES = (24000, 22050, 16000, 12000, 11025, 8000)
|
||||
|
||||
|
||||
class EncodeError(ValueError):
|
||||
pass
|
||||
|
||||
|
||||
def vorbis_ogg(x, rate, quality):
|
||||
"""Encode float32 PCM to Ogg Vorbis bytes at libvorbis quality `quality` (0..10)."""
|
||||
b = io.BytesIO()
|
||||
level = min(1.0, max(0.0, 1.0 - quality / 10.0))
|
||||
x = np.ascontiguousarray(audio._as2d(x))
|
||||
# write in blocks: libsndfile's Vorbis writer overflows the C stack on very large single writes
|
||||
with sf.SoundFile(b, "w", rate, x.shape[1], format="OGG", subtype="VORBIS", compression_level=level) as f:
|
||||
for i in range(0, len(x), 4096):
|
||||
f.write(x[i:i + 4096])
|
||||
return b.getvalue()
|
||||
|
||||
|
||||
# --- setup probing (which q values are usable at a rate/channel count) ------------------------
|
||||
_cache = None
|
||||
|
||||
|
||||
def _cache_path():
|
||||
return os.path.join(user_dir("cache"), "vorbis_probe.json")
|
||||
|
||||
|
||||
def _probe_cache():
|
||||
global _cache
|
||||
if _cache is None:
|
||||
try:
|
||||
c = json.load(open(_cache_path()))
|
||||
_cache = c if c.get("_lib") == sf.__libsndfile_version__ else None
|
||||
except (OSError, ValueError):
|
||||
_cache = None
|
||||
_cache = _cache or {"_lib": sf.__libsndfile_version__}
|
||||
return _cache
|
||||
|
||||
|
||||
def probe_setup(rate, channels, quality):
|
||||
"""(stripped Wwise setup or None, error) for libvorbis at these settings (cached)."""
|
||||
key = "%d,%d,%.1f" % (rate, channels, quality)
|
||||
c = _probe_cache()
|
||||
if key not in c:
|
||||
noise = (np.random.default_rng(7).standard_normal((rate, channels)) * 0.1).astype(np.float32)
|
||||
pk = read_packets(vorbis_ogg(noise, rate, quality))
|
||||
try:
|
||||
s, _ = transcode_setup(pk[2][0], V2W, channels, library())
|
||||
c[key] = [s.hex(), None]
|
||||
except ValueError as e:
|
||||
c[key] = [None, str(e)]
|
||||
try:
|
||||
json.dump(c, open(_cache_path(), "w"))
|
||||
except OSError:
|
||||
pass
|
||||
s, err = c[key]
|
||||
return (bytes.fromhex(s) if s else None), err
|
||||
|
||||
|
||||
def choose_quality(rate, channels, quality="auto"):
|
||||
"""-> (quality, known) for a rate/channel count, or raise EncodeError."""
|
||||
if quality != "auto":
|
||||
q = float(quality)
|
||||
s, err = probe_setup(rate, channels, q)
|
||||
if s is None:
|
||||
raise EncodeError("quality %s cannot be used at %d Hz / %d ch: %s" % (quality, rate, channels, err))
|
||||
return q, bool(lookup_setup(s))
|
||||
compatible = None
|
||||
for q in [x for x in QUALITIES if x <= AUTO_MAX_Q_KNOWN]:
|
||||
s, _ = probe_setup(rate, channels, q)
|
||||
if s is None:
|
||||
continue
|
||||
if lookup_setup(s):
|
||||
return q, True
|
||||
if compatible is None and q <= AUTO_MAX_Q:
|
||||
compatible = q
|
||||
if compatible is None:
|
||||
raise EncodeError("no libvorbis quality yields game-compatible codebooks at %d Hz / %d ch" % (rate, channels))
|
||||
return compatible, False
|
||||
|
||||
|
||||
def pick_format(rate, channels, quality="auto"):
|
||||
"""Find a usable (rate, quality). Falls back to lower rates when needed (Wwise resamples every
|
||||
source, so playback speed is unaffected)."""
|
||||
try:
|
||||
return (rate,) + choose_quality(rate, channels, quality)
|
||||
except EncodeError as first:
|
||||
for r in FALLBACK_RATES:
|
||||
if r < rate:
|
||||
try:
|
||||
return (r,) + choose_quality(r, channels, quality)
|
||||
except EncodeError:
|
||||
pass
|
||||
raise first
|
||||
|
||||
|
||||
def encode(x, src_rate, rate, channels, quality="auto"):
|
||||
"""PCM -> WEM bytes in the requested format. Returns (wem, info)."""
|
||||
r, q, known = pick_format(rate, channels, quality)
|
||||
pcm = audio.conform(x, src_rate, r, channels)
|
||||
if len(pcm) < 64:
|
||||
raise EncodeError("the audio is empty")
|
||||
wem = ogg_to_wem(vorbis_ogg(pcm, r, q))
|
||||
return wem, dict(rate=r, quality=q, known_setup=known, size=len(wem), channels=channels,
|
||||
seconds=len(pcm) / float(r), requested_rate=rate)
|
||||
|
||||
|
||||
def encode_fit(x, src_rate, rate, channels, limit=None, quality="auto", progress=None):
|
||||
"""Like `encode`, but if the result is larger than `limit` bytes lower the quality step by step,
|
||||
then the sample rate, until it fits. Raises EncodeError if it cannot fit."""
|
||||
wem, info = encode(x, src_rate, rate, channels, quality)
|
||||
if limit is None or len(wem) <= limit:
|
||||
return wem, info
|
||||
first = len(wem)
|
||||
for r in [info["rate"]] + [f for f in FIT_RATES if f < info["rate"]]:
|
||||
pcm = audio.conform(x, src_rate, r, channels)
|
||||
for q in QUALITIES:
|
||||
if r == info["rate"] and q >= info["quality"]:
|
||||
continue
|
||||
if probe_setup(r, channels, q)[0] is None:
|
||||
continue
|
||||
if progress:
|
||||
progress("shrinking: %d Hz, quality %.1f" % (r, q))
|
||||
w = ogg_to_wem(vorbis_ogg(pcm, r, q))
|
||||
if len(w) <= limit:
|
||||
info.update(rate=r, quality=q, size=len(w), shrunk_from=first,
|
||||
known_setup=bool(lookup_setup(probe_setup(r, channels, q)[0])))
|
||||
return w, info
|
||||
raise EncodeError("the audio is too long for this sound even at the lowest quality "
|
||||
"(%d bytes, maximum %d). Shorten it." % (first, limit))
|
||||
205
ackit/game.py
Normal file
205
ackit/game.py
Normal file
@@ -0,0 +1,205 @@
|
||||
"""The game installation: locating it (Steam on Windows / Linux / Proton), its WADs and language.
|
||||
|
||||
A `Game` is built from any folder path; nothing here is hard-coded to one machine.
|
||||
"""
|
||||
import glob
|
||||
import hashlib
|
||||
import os
|
||||
import re
|
||||
import struct
|
||||
import sys
|
||||
|
||||
APPID = 200260
|
||||
FOLDER_NAMES = ("Batman Arkham City GOTY", "Batman Arkham City")
|
||||
LANGS = ("INT", "ITA", "FRA", "DEU", "ESN", "ESM", "POL", "RUS", "JPN", "KOR", "POR")
|
||||
|
||||
|
||||
class GameError(Exception):
|
||||
"""User-facing problem with the game folder (message is meant to be shown as-is)."""
|
||||
|
||||
|
||||
def _iglob_one(base, *parts):
|
||||
"""Case-insensitive path join (Linux file systems are case sensitive)."""
|
||||
cur = base
|
||||
for p in parts:
|
||||
if not os.path.isdir(cur):
|
||||
return None
|
||||
hit = next((e for e in os.listdir(cur) if e.lower() == p.lower()), None)
|
||||
if hit is None:
|
||||
return None
|
||||
cur = os.path.join(cur, hit)
|
||||
return cur
|
||||
|
||||
|
||||
# --- Steam discovery ------------------------------------------------------------------------
|
||||
def steam_roots():
|
||||
roots = []
|
||||
if sys.platform == "win32":
|
||||
try:
|
||||
import winreg
|
||||
with winreg.OpenKey(winreg.HKEY_CURRENT_USER, r"Software\Valve\Steam") as k:
|
||||
roots.append(winreg.QueryValueEx(k, "SteamPath")[0])
|
||||
except OSError:
|
||||
pass
|
||||
roots += [r"C:\Program Files (x86)\Steam", r"C:\Program Files\Steam"]
|
||||
else:
|
||||
h = os.path.expanduser("~")
|
||||
roots += [os.path.join(h, ".steam", "steam"), os.path.join(h, ".local", "share", "Steam"),
|
||||
os.path.join(h, ".var", "app", "com.valvesoftware.Steam", ".local", "share", "Steam"),
|
||||
os.path.join(h, "snap", "steam", "common", ".local", "share", "Steam")]
|
||||
out = []
|
||||
for r in roots:
|
||||
r = os.path.normpath(r)
|
||||
if os.path.isdir(r) and r not in out:
|
||||
out.append(r)
|
||||
return out
|
||||
|
||||
|
||||
def steam_libraries():
|
||||
libs = []
|
||||
for root in steam_roots():
|
||||
libs.append(root)
|
||||
vdf = _iglob_one(root, "steamapps", "libraryfolders.vdf")
|
||||
if vdf:
|
||||
for m in re.finditer(r'"path"\s+"([^"]+)"', open(vdf, encoding="utf-8", errors="replace").read()):
|
||||
libs.append(m.group(1).replace("\\\\", "\\"))
|
||||
seen, out = set(), []
|
||||
for l in libs:
|
||||
n = os.path.normcase(os.path.normpath(l))
|
||||
if n not in seen and os.path.isdir(l):
|
||||
seen.add(n)
|
||||
out.append(os.path.normpath(l))
|
||||
return out
|
||||
|
||||
|
||||
def _true_case(path):
|
||||
"""Windows paths from the registry may be lower-cased; rebuild the real spelling."""
|
||||
path = os.path.normpath(path)
|
||||
drive, rest = os.path.splitdrive(path)
|
||||
cur = drive.upper() + os.sep if drive else os.sep
|
||||
for part in [p for p in rest.split(os.sep) if p]:
|
||||
hit = next((e for e in os.listdir(cur) if e.lower() == part.lower()), part) if os.path.isdir(cur) else part
|
||||
cur = os.path.join(cur, hit)
|
||||
return cur
|
||||
|
||||
|
||||
def find_installs():
|
||||
"""All Arkham City folders found in Steam libraries."""
|
||||
out = []
|
||||
for lib in steam_libraries():
|
||||
for name in FOLDER_NAMES:
|
||||
p = _iglob_one(lib, "steamapps", "common", name)
|
||||
if p and is_game_dir(p) and _true_case(p) not in out:
|
||||
out.append(_true_case(p))
|
||||
return out
|
||||
|
||||
|
||||
def is_game_dir(path):
|
||||
return bool(path) and _iglob_one(path, "BmGame", "CookedPCConsole") is not None
|
||||
|
||||
|
||||
# --- Game ------------------------------------------------------------------------------------
|
||||
class Game:
|
||||
def __init__(self, root):
|
||||
root = os.path.normpath(os.path.abspath(root))
|
||||
if not is_game_dir(root):
|
||||
raise GameError("'%s' is not a Batman: Arkham City folder (BmGame\\CookedPCConsole is missing)." % root)
|
||||
self.root = root
|
||||
self.cooked = _iglob_one(root, "BmGame", "CookedPCConsole")
|
||||
self.archives = self._find_archives()
|
||||
if not self.archives:
|
||||
raise GameError("No audio archives (.wad) found in %s." % self.cooked)
|
||||
|
||||
def _find_archives(self):
|
||||
arch = {}
|
||||
for p in glob.glob(os.path.join(self.cooked, "*", "*.wad")):
|
||||
m = re.search(r"_LOC_([A-Za-z]{3})\.wad$", p)
|
||||
if m:
|
||||
arch[m.group(1).upper()] = p
|
||||
elif os.path.basename(os.path.dirname(p)).upper() == "SFX":
|
||||
arch["SFX"] = p
|
||||
return dict(sorted(arch.items()))
|
||||
|
||||
@property
|
||||
def languages(self):
|
||||
return [a for a in self.archives if a != "SFX"]
|
||||
|
||||
def wad(self, archive):
|
||||
try:
|
||||
return self.archives[archive.upper()]
|
||||
except KeyError:
|
||||
raise GameError("This game has no '%s' audio archive (available: %s)."
|
||||
% (archive, ", ".join(self.archives)))
|
||||
|
||||
def rel(self, path):
|
||||
return os.path.relpath(path, self.root).replace("\\", "/")
|
||||
|
||||
@property
|
||||
def exe(self):
|
||||
return _iglob_one(self.root, "Binaries", "Win32", "BatmanAC.exe")
|
||||
|
||||
@property
|
||||
def store_id(self):
|
||||
"""Stable id of this install, used to name its originals store."""
|
||||
return hashlib.sha1(os.path.normcase(self.root).encode("utf-8")).hexdigest()[:12]
|
||||
|
||||
# language -------------------------------------------------------------------------------
|
||||
def config_dirs(self):
|
||||
"""Possible BmGame\\Config folders (Windows Documents, Proton prefix)."""
|
||||
dirs = [os.path.join(os.path.expanduser("~"), "Documents", "WB Games", "Batman Arkham City GOTY",
|
||||
"BmGame", "Config")]
|
||||
steamapps = os.path.dirname(os.path.dirname(self.root)) # .../steamapps
|
||||
pfx = os.path.join(steamapps, "compatdata", str(APPID), "pfx", "drive_c", "users", "steamuser")
|
||||
for docs in ("Documents", "My Documents"):
|
||||
dirs.append(os.path.join(pfx, docs, "WB Games", "Batman Arkham City GOTY", "BmGame", "Config"))
|
||||
return [d for d in dirs if os.path.isdir(d)]
|
||||
|
||||
def language(self):
|
||||
"""Speech language the game is set to (falls back to INT, then the first available)."""
|
||||
for d in self.config_dirs():
|
||||
for f in ("UserEngine.ini", "BmEngine.ini"):
|
||||
p = os.path.join(d, f)
|
||||
if not os.path.exists(p):
|
||||
continue
|
||||
raw = open(p, "rb").read()
|
||||
txt = raw.decode("utf-16") if raw[:2] in (b"\xff\xfe", b"\xfe\xff") else raw.decode("latin-1")
|
||||
m = re.search(r"^\s*Language\s*=\s*(\w+)", txt, re.M | re.I)
|
||||
if m and m.group(1).upper() in self.archives:
|
||||
return m.group(1).upper()
|
||||
if "INT" in self.archives:
|
||||
return "INT"
|
||||
return self.languages[0] if self.languages else "SFX"
|
||||
|
||||
# safety -----------------------------------------------------------------------------------
|
||||
def check_writable(self, path):
|
||||
"""Raise GameError if the file cannot be replaced (game running, read-only, ...)."""
|
||||
try:
|
||||
with open(path, "r+b"):
|
||||
pass
|
||||
except PermissionError:
|
||||
raise GameError("Cannot write %s.\nClose the game (and anything else using the file) and try again."
|
||||
% os.path.basename(path))
|
||||
d = os.path.dirname(path)
|
||||
if not os.access(d, os.W_OK):
|
||||
raise GameError("The folder %s is not writable." % d)
|
||||
|
||||
|
||||
def wad_fingerprint(path):
|
||||
"""Identity of a WAD's game build: SHA-1 over its entry IDs and the sizes of all entries except
|
||||
stream packs (whose size legitimately changes when music is replaced). In-place sound edits do not
|
||||
change it; a different game build or language does."""
|
||||
from .inject import is_pack
|
||||
from .wad import Wad
|
||||
w = Wad(path)
|
||||
h = hashlib.sha1()
|
||||
for e in w.entries:
|
||||
h.update(struct.pack("<II", e.hash, 0 if is_pack(path, e.hash) else e.size))
|
||||
return "%d-%s" % (len(w.entries), h.hexdigest()[:16])
|
||||
|
||||
|
||||
def default_game_dir(settings=None):
|
||||
"""Last used folder from settings, else the first Steam install found."""
|
||||
if settings and settings.get("game_dir") and is_game_dir(settings["game_dir"]):
|
||||
return settings["game_dir"]
|
||||
found = find_installs()
|
||||
return found[0] if found else None
|
||||
133
ackit/inject.py
Normal file
133
ackit/inject.py
Normal file
@@ -0,0 +1,133 @@
|
||||
"""Read and replace WEMs inside WAD archives.
|
||||
|
||||
Target syntax (stable ids used everywhere, also in mod packages):
|
||||
'HASH' WAD entry holding one WEM e.g. 800BE4F0
|
||||
'HASH:i' i-th WEM (0-based) of an entry holding several e.g. B49CDF48:11
|
||||
'PACK/MEDIA' streamed WEM inside a BKPK stream pack (SFX) e.g. EDF036D6/393BC53A
|
||||
|
||||
Rules (see docs/formats/wad.md):
|
||||
* entries / variations are indexed from inside the game's compressed .upk packages, so they are
|
||||
replaced IN PLACE and must not exceed the original WEM size (`limit`); the WAD layout never moves;
|
||||
* stream-pack media may change size: the pack index (SFX\\<PACK>.bnk) is rewritten too.
|
||||
Writes go to a temporary file which atomically replaces the original (never half-written).
|
||||
"""
|
||||
import os
|
||||
|
||||
from .pack import Pack
|
||||
from .wad import Wad, write_wad
|
||||
from .wem import Wem, split_concatenated
|
||||
|
||||
|
||||
class SizeLimitError(ValueError):
|
||||
def __init__(self, target, limit, size):
|
||||
super().__init__("the new sound is %d bytes but the original is %d bytes; the game reads exactly the "
|
||||
"original size, so the replacement must not be larger (shorten the audio or lower "
|
||||
"the quality)" % (size, limit))
|
||||
self.target, self.limit, self.size = target, limit, size
|
||||
|
||||
|
||||
def parse_target(t: str):
|
||||
"""-> (entry_hash, index, media_id or None)"""
|
||||
if "/" in t:
|
||||
p, m = t.split("/", 1)
|
||||
return int(p, 16), 0, int(m, 16)
|
||||
h, _, idx = t.partition(":")
|
||||
return int(h, 16), int(idx or 0), None
|
||||
|
||||
|
||||
def pack_bnk_path(wad_path, pack_id):
|
||||
"""SFX\\<PACK>.bnk next to the WAD (case-insensitive), or None."""
|
||||
d = os.path.dirname(wad_path)
|
||||
want = ("%08X.bnk" % pack_id).lower()
|
||||
hit = next((f for f in os.listdir(d) if f.lower() == want), None)
|
||||
return os.path.join(d, hit) if hit else None
|
||||
|
||||
|
||||
def is_pack(wad_path, entry_hash):
|
||||
p = pack_bnk_path(wad_path, entry_hash)
|
||||
if not p:
|
||||
return False
|
||||
with open(p, "rb") as f:
|
||||
return f.read(4) == b"BKPK"
|
||||
|
||||
|
||||
def read_target(wad_path: str, target: str, wad: Wad = None) -> bytes:
|
||||
"""Bytes of one WEM (seeks directly; does not load whole packs)."""
|
||||
wad = wad or Wad(wad_path)
|
||||
h, idx, mid = parse_target(target)
|
||||
if h not in wad.index:
|
||||
raise KeyError("entry %08X not in %s" % (h, os.path.basename(wad_path)))
|
||||
e = wad.index[h]
|
||||
if mid is not None:
|
||||
pk = Pack(open(pack_bnk_path(wad_path, h), "rb").read())
|
||||
_, size, off = pk.entries[pk.index[mid]]
|
||||
with open(wad_path, "rb") as f:
|
||||
f.seek(e.offset + off)
|
||||
return f.read(size)
|
||||
riffs, _ = split_concatenated(wad.read(h))
|
||||
if idx >= len(riffs):
|
||||
raise IndexError("entry %08X holds %d WEM(s)" % (h, len(riffs)))
|
||||
return riffs[idx][1]
|
||||
|
||||
|
||||
def replace_entry_payload(payload: bytes, index: int, new_wem: bytes, limit: int, target="") -> bytes:
|
||||
"""Replace WEM `index` in place, zero-filled up to `limit` bytes; nothing else moves."""
|
||||
riffs, _ = split_concatenated(payload)
|
||||
if index >= len(riffs):
|
||||
raise IndexError("entry holds %d WEM(s); index %d invalid" % (len(riffs), index))
|
||||
if len(new_wem) > limit:
|
||||
raise SizeLimitError(target, limit, len(new_wem))
|
||||
off = riffs[index][0]
|
||||
if off + limit > len(payload):
|
||||
raise ValueError("entry is shorter than the original size limit (unexpected WAD state)")
|
||||
return payload[:off] + new_wem + bytes(limit - len(new_wem)) + payload[off + limit:]
|
||||
|
||||
|
||||
def inject(wad_path: str, replacements: dict, limits: dict, out_path: str = None, pack_gaps: dict = None):
|
||||
"""Write several WEMs into one WAD in a single atomic rewrite.
|
||||
|
||||
replacements: {target: wem_bytes}; limits: {target: original size} for non-pack targets.
|
||||
out_path: write a copy elsewhere instead of replacing the WAD (tests/dry runs).
|
||||
pack_gaps: {pack hash: Pack.gaps()} original padding, so reverts restore packs byte-for-byte.
|
||||
Returns the list of files written."""
|
||||
src = Wad(wad_path)
|
||||
payloads, pack_repl = {}, {}
|
||||
for target, data in replacements.items():
|
||||
Wem.parse(data) # must be a game-format WEM
|
||||
h, idx, mid = parse_target(target)
|
||||
if h not in src.index:
|
||||
raise KeyError("entry %08X not in %s" % (h, os.path.basename(wad_path)))
|
||||
if mid is not None:
|
||||
pack_repl.setdefault(h, {})[mid] = data
|
||||
continue
|
||||
if limits.get(target) is None:
|
||||
raise ValueError("no size limit given for %s" % target)
|
||||
base = payloads.get(h) or src.read(h)
|
||||
payloads[h] = replace_entry_payload(base, idx, data, limits[target], target)
|
||||
new_bnks = {}
|
||||
for h, media in pack_repl.items():
|
||||
bpath = pack_bnk_path(wad_path, h)
|
||||
new_bnks[bpath], payloads[h] = Pack(open(bpath, "rb").read()).rebuild(
|
||||
src.read(h), media, (pack_gaps or {}).get(h))
|
||||
|
||||
dst = out_path or wad_path
|
||||
bnk_dsts = {b: (b if out_path is None else os.path.join(os.path.dirname(out_path), os.path.basename(b)))
|
||||
for b in new_bnks}
|
||||
tmps = [dst + ".ackit-tmp"] + [d + ".ackit-tmp" for d in bnk_dsts.values()]
|
||||
try:
|
||||
write_wad(src, tmps[0], payloads)
|
||||
problems = Wad(tmps[0]).check()
|
||||
if problems:
|
||||
raise RuntimeError("rebuilt WAD failed validation: %s" % problems[:3])
|
||||
for b, d in bnk_dsts.items():
|
||||
with open(d + ".ackit-tmp", "wb") as f:
|
||||
f.write(new_bnks[b])
|
||||
# commit: pack indexes first (tiny), then the WAD; each os.replace is atomic
|
||||
for d in bnk_dsts.values():
|
||||
os.replace(d + ".ackit-tmp", d)
|
||||
os.replace(tmps[0], dst)
|
||||
finally:
|
||||
for p in tmps:
|
||||
if os.path.exists(p):
|
||||
os.remove(p)
|
||||
return [dst] + list(bnk_dsts.values())
|
||||
25
ackit/known_setups.py
Normal file
25
ackit/known_setups.py
Normal file
@@ -0,0 +1,25 @@
|
||||
"""Database of Vorbis setups used by the game's own WEMs (from all shipped WADs).
|
||||
|
||||
Each entry: stripped Wwise setup (hex), its codebook hash, decode alloc sizes, and
|
||||
formats seen. Reusing a known setup guarantees hash/alloc values the game accepts.
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
|
||||
PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "data", "known_setups.json")
|
||||
_DB = None
|
||||
|
||||
|
||||
def db():
|
||||
global _DB
|
||||
if _DB is None:
|
||||
_DB = json.load(open(PATH))
|
||||
return _DB
|
||||
|
||||
|
||||
def lookup_setup(ww_setup: bytes):
|
||||
h = ww_setup.hex()
|
||||
for e in db():
|
||||
if e["setup"] == h:
|
||||
return e
|
||||
return None
|
||||
120
ackit/modpack.py
Normal file
120
ackit/modpack.py
Normal file
@@ -0,0 +1,120 @@
|
||||
"""Portable mod packages (*.acmod): share replaced sounds between machines and languages.
|
||||
|
||||
A package is a zip:
|
||||
manifest.json {"format": 1, "name", "author", "description", "created", "items": [...]}
|
||||
items/NNN.wem the exact WEM that was applied (used as-is when the target matches)
|
||||
items/NNN.flac the same audio, decoded (used to re-encode when the target differs, e.g. another
|
||||
language whose original has a different size)
|
||||
Each item records archive, target, name, group and the SHA-1 of the original it replaced.
|
||||
"""
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
import zipfile
|
||||
from dataclasses import dataclass
|
||||
|
||||
import soundfile as sf
|
||||
|
||||
from . import audio
|
||||
from .originals import sha1_16
|
||||
|
||||
FORMAT = 1
|
||||
|
||||
|
||||
def export_mod(ws, path, keys, name="", author="", description=""):
|
||||
"""Write the applied versions of `keys` (from the originals store) into a package."""
|
||||
items = []
|
||||
with zipfile.ZipFile(path + ".tmp", "w", zipfile.ZIP_DEFLATED) as z:
|
||||
for i, key in enumerate(keys):
|
||||
e = ws.store.get(key)
|
||||
if not e or not e.get("applied"):
|
||||
raise ValueError("%s is not modified" % key)
|
||||
s = ws.catalog.by_key.get(key)
|
||||
wem = ws.store.get_blob(e["applied"])
|
||||
x, sr = audio.decode_wem(wem)
|
||||
buf = io.BytesIO()
|
||||
sf.write(buf, x, sr, format="FLAC")
|
||||
base = "items/%03d" % i
|
||||
z.writestr(base + ".wem", wem)
|
||||
z.writestr(base + ".flac", buf.getvalue())
|
||||
items.append({"archive": e["archive"], "target": e["target"], "name": s.name if s else "",
|
||||
"group": s.group if s else "", "original_sha1": e["orig_sha1"],
|
||||
"wem": base + ".wem", "audio": base + ".flac", "meta": e.get("meta", {})})
|
||||
z.writestr("manifest.json", json.dumps({"format": FORMAT, "name": name, "author": author,
|
||||
"description": description, "created": time.time(),
|
||||
"game": "Batman: Arkham City (PC)", "items": items}, indent=1))
|
||||
os.replace(path + ".tmp", path)
|
||||
return len(items)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ImportItem:
|
||||
item: dict
|
||||
sound: object = None # matched Sound or None
|
||||
how: str = "" # 'exact' | 'by name' | 'missing'
|
||||
wem: bytes = None # ready-to-apply bytes (after `prepare_import`)
|
||||
note: str = ""
|
||||
|
||||
|
||||
def read_mod(path):
|
||||
z = zipfile.ZipFile(path)
|
||||
man = json.loads(z.read("manifest.json"))
|
||||
if man.get("format") != FORMAT:
|
||||
raise ValueError("unsupported mod package format %r" % man.get("format"))
|
||||
return z, man
|
||||
|
||||
|
||||
def match_mod(ws, path):
|
||||
"""Match package items to sounds of this workspace. -> (manifest, [ImportItem])"""
|
||||
z, man = read_mod(path)
|
||||
out = []
|
||||
for it in man["items"]:
|
||||
key = "%s:%s" % (it["archive"], it["target"])
|
||||
s = ws.catalog.by_key.get(key)
|
||||
if s and it.get("name") and s.name != it["name"]:
|
||||
s = None
|
||||
if s:
|
||||
out.append(ImportItem(it, s, "exact"))
|
||||
continue
|
||||
try:
|
||||
cands = [c for c in ws.catalog.search(it["name"]) if c.name == it["name"]] if it.get("name") else []
|
||||
except LookupError:
|
||||
cands = []
|
||||
if len(cands) == 1:
|
||||
out.append(ImportItem(it, cands[0], "by name"))
|
||||
else:
|
||||
out.append(ImportItem(it, None, "missing", note="no sound named %r in this game/language" % it.get("name")))
|
||||
z.close()
|
||||
return man, out
|
||||
|
||||
|
||||
def prepare_import(ws, path, items, progress=None):
|
||||
"""Fill `wem` for matched items: reuse the packaged WEM when it targets the very same original,
|
||||
otherwise re-encode the packaged audio for the local sound."""
|
||||
z, _ = read_mod(path)
|
||||
for im in items:
|
||||
if not im.sound:
|
||||
continue
|
||||
orig = ws.original_bytes(im.sound)
|
||||
wem = z.read(im.item["wem"])
|
||||
lim = ws.limit(im.sound)
|
||||
if im.how == "exact" and sha1_16(orig) == im.item.get("original_sha1") and (lim is None or len(wem) <= lim):
|
||||
im.wem, im.note = wem, "exact copy"
|
||||
else:
|
||||
x, sr = sf.read(io.BytesIO(z.read(im.item["audio"])), dtype="float32", always_2d=True)
|
||||
im.wem, info = ws.prepare(im.sound, x, sr, progress=progress)
|
||||
im.note = "re-encoded (%d Hz, q%.1f)" % (info["rate"], info["quality"])
|
||||
z.close()
|
||||
return items
|
||||
|
||||
|
||||
def import_mod(ws, path, force=False, progress=None):
|
||||
"""Match, prepare and apply a package. Returns the list of ImportItem (see .how/.note)."""
|
||||
_, items = match_mod(ws, path)
|
||||
prepare_import(ws, path, items, progress)
|
||||
changes = [(im.sound, im.wem, {"source": "mod:" + path.replace("\\", "/").split("/")[-1]})
|
||||
for im in items if im.wem]
|
||||
if changes:
|
||||
ws.apply(changes, force=force, progress=progress)
|
||||
return items
|
||||
72
ackit/ogg.py
Normal file
72
ackit/ogg.py
Normal file
@@ -0,0 +1,72 @@
|
||||
"""Minimal Ogg page reader/writer."""
|
||||
import struct
|
||||
|
||||
|
||||
def _crc_table():
|
||||
t = []
|
||||
for i in range(256):
|
||||
r = i << 24
|
||||
for _ in range(8):
|
||||
r = ((r << 1) ^ 0x04C11DB7) if r & 0x80000000 else (r << 1)
|
||||
t.append(r & 0xFFFFFFFF)
|
||||
return t
|
||||
|
||||
|
||||
_T = _crc_table()
|
||||
|
||||
|
||||
def ogg_crc(b: bytes) -> int:
|
||||
c = 0
|
||||
for x in b:
|
||||
c = ((c << 8) & 0xFFFFFFFF) ^ _T[((c >> 24) & 0xFF) ^ x]
|
||||
return c
|
||||
|
||||
|
||||
def read_packets(b: bytes):
|
||||
"""Return list of (packet_bytes, granule_of_page_if_packet_ends_page_else_None)."""
|
||||
pos = 0
|
||||
out = []
|
||||
cur = b""
|
||||
while pos + 27 <= len(b):
|
||||
if b[pos:pos + 4] != b"OggS":
|
||||
raise ValueError("lost Ogg sync at %d" % pos)
|
||||
htype, gran, serial, seq, crc, nseg = struct.unpack_from("<BqIIIB", b, pos + 5)
|
||||
segs = b[pos + 27: pos + 27 + nseg]
|
||||
p = pos + 27 + nseg
|
||||
ends = []
|
||||
for s in segs:
|
||||
cur += b[p:p + s]
|
||||
p += s
|
||||
if s < 255:
|
||||
ends.append(cur)
|
||||
cur = b""
|
||||
for i, pk in enumerate(ends):
|
||||
out.append((pk, gran if i == len(ends) - 1 else None))
|
||||
pos = p
|
||||
return out
|
||||
|
||||
|
||||
class OggWriter:
|
||||
def __init__(self, serial=0x41434B54):
|
||||
self.serial = serial
|
||||
self.seq = 0
|
||||
self.out = bytearray()
|
||||
|
||||
def page(self, packets, granule, bos=False, eos=False):
|
||||
segs = bytearray()
|
||||
body = bytearray()
|
||||
for pk in packets:
|
||||
n = len(pk)
|
||||
while n >= 255:
|
||||
segs.append(255); n -= 255
|
||||
segs.append(n)
|
||||
body += pk
|
||||
hdr = struct.pack("<4sBBqIIIB", b"OggS", 0, (2 if bos else 0) | (4 if eos else 0),
|
||||
granule, self.serial, self.seq, 0, len(segs)) + bytes(segs)
|
||||
page = bytearray(hdr + body)
|
||||
struct.pack_into("<I", page, 22, ogg_crc(page))
|
||||
self.out += page
|
||||
self.seq += 1
|
||||
|
||||
def bytes(self):
|
||||
return bytes(self.out)
|
||||
143
ackit/originals.py
Normal file
143
ackit/originals.py
Normal file
@@ -0,0 +1,143 @@
|
||||
"""Originals store: keeps the game's original bytes of every sound you modify, so any change can be
|
||||
reverted exactly, without keeping full copies of the 0.4-0.5 GB WADs.
|
||||
|
||||
Layout (one store per game folder, in the user data folder by default; see docs/decisions.md):
|
||||
<root>/store.json records: {key: {...}} + per-WAD info
|
||||
<root>/blobs/<sha256>.wem original WEMs and the replacements you applied
|
||||
<root>/gaps/<PACK>.json original non-zero padding of stream packs (byte-exact pack reverts)
|
||||
<root>/full/<rel path> optional full copies of WADs / pack indexes (fallback, see `full_backup`)
|
||||
Writes to store.json are atomic (temp file + rename).
|
||||
"""
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import time
|
||||
|
||||
STORE_VERSION = 1
|
||||
|
||||
|
||||
def sha256(b):
|
||||
return hashlib.sha256(b).hexdigest()
|
||||
|
||||
|
||||
def sha1_16(b):
|
||||
return hashlib.sha1(b).hexdigest()[:16]
|
||||
|
||||
|
||||
class OriginalsStore:
|
||||
def __init__(self, root, game_root=None):
|
||||
self.root = root
|
||||
os.makedirs(os.path.join(root, "blobs"), exist_ok=True)
|
||||
self.path = os.path.join(root, "store.json")
|
||||
if os.path.exists(self.path):
|
||||
self.data = json.load(open(self.path, encoding="utf-8"))
|
||||
else:
|
||||
self.data = {"version": STORE_VERSION, "game_root": game_root, "created": time.time(),
|
||||
"entries": {}, "wads": {}}
|
||||
self.save()
|
||||
|
||||
# persistence -------------------------------------------------------------------------------
|
||||
def save(self):
|
||||
tmp = self.path + ".tmp"
|
||||
with open(tmp, "w", encoding="utf-8") as f:
|
||||
json.dump(self.data, f, indent=1)
|
||||
os.replace(tmp, self.path)
|
||||
|
||||
def put_blob(self, b):
|
||||
h = sha256(b)
|
||||
p = os.path.join(self.root, "blobs", h + ".wem")
|
||||
if not os.path.exists(p):
|
||||
tmp = p + ".tmp"
|
||||
with open(tmp, "wb") as f:
|
||||
f.write(b)
|
||||
os.replace(tmp, p)
|
||||
return h
|
||||
|
||||
def get_blob(self, h):
|
||||
p = os.path.join(self.root, "blobs", h + ".wem")
|
||||
b = open(p, "rb").read()
|
||||
if sha256(b) != h:
|
||||
raise IOError("originals store is damaged: %s" % p)
|
||||
return b
|
||||
|
||||
# records ---------------------------------------------------------------------------------
|
||||
@property
|
||||
def entries(self):
|
||||
return self.data["entries"]
|
||||
|
||||
def get(self, key):
|
||||
return self.entries.get(key)
|
||||
|
||||
def original(self, key):
|
||||
e = self.get(key)
|
||||
return self.get_blob(e["orig"]) if e else None
|
||||
|
||||
def remember_original(self, key, archive, target, wad_rel, data):
|
||||
"""Store the original bytes the first time a sound is modified (no-op afterwards)."""
|
||||
if key not in self.entries:
|
||||
self.entries[key] = {"archive": archive, "target": target, "wad": wad_rel,
|
||||
"orig": self.put_blob(data), "orig_size": len(data),
|
||||
"orig_sha1": sha1_16(data), "first_modified": time.time(),
|
||||
"applied": None}
|
||||
return self.entries[key]
|
||||
|
||||
def record_applied(self, key, data, meta=None):
|
||||
e = self.entries[key]
|
||||
e["applied"] = self.put_blob(data)
|
||||
e["applied_at"] = time.time()
|
||||
e["meta"] = meta or {}
|
||||
|
||||
def forget(self, key):
|
||||
self.entries.pop(key, None)
|
||||
|
||||
def gc(self):
|
||||
"""Delete blobs no record refers to."""
|
||||
used = {e[k] for e in self.entries.values() for k in ("orig", "applied") if e.get(k)}
|
||||
d = os.path.join(self.root, "blobs")
|
||||
for f in os.listdir(d):
|
||||
if f.endswith(".wem") and f[:-4] not in used:
|
||||
os.remove(os.path.join(d, f))
|
||||
|
||||
# pack padding ------------------------------------------------------------------------------
|
||||
def pack_gaps(self, pack_id):
|
||||
p = os.path.join(self.root, "gaps", "%08X.json" % pack_id)
|
||||
if not os.path.exists(p):
|
||||
return None
|
||||
return {int(k): bytes.fromhex(v) for k, v in json.load(open(p)).items()}
|
||||
|
||||
def remember_pack_gaps(self, pack_id, gaps):
|
||||
p = os.path.join(self.root, "gaps", "%08X.json" % pack_id)
|
||||
if os.path.exists(p):
|
||||
return
|
||||
os.makedirs(os.path.dirname(p), exist_ok=True)
|
||||
json.dump({str(k): v.hex() for k, v in gaps.items()}, open(p, "w"))
|
||||
|
||||
# full-file fallback backups ----------------------------------------------------------------
|
||||
def full_backup(self, src, rel):
|
||||
"""Copy a game file once (before its first modification), verified by SHA-1."""
|
||||
dst = os.path.join(self.root, "full", rel)
|
||||
if os.path.exists(dst):
|
||||
return dst
|
||||
os.makedirs(os.path.dirname(dst), exist_ok=True)
|
||||
tmp = dst + ".tmp"
|
||||
shutil.copy2(src, tmp)
|
||||
if _file_sha1(tmp) != _file_sha1(src):
|
||||
os.remove(tmp)
|
||||
raise IOError("backup copy of %s does not match the original" % rel)
|
||||
os.replace(tmp, dst)
|
||||
self.data.setdefault("full", {})[rel] = {"size": os.path.getsize(dst), "time": time.time()}
|
||||
self.save()
|
||||
return dst
|
||||
|
||||
def full_copy(self, rel):
|
||||
p = os.path.join(self.root, "full", rel)
|
||||
return p if os.path.exists(p) else None
|
||||
|
||||
|
||||
def _file_sha1(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()
|
||||
92
ackit/pack.py
Normal file
92
ackit/pack.py
Normal file
@@ -0,0 +1,92 @@
|
||||
"""BKPK stream packs: SFX\\<ID>.bnk ("BKPK" header + embedded bank) indexes the streamed WEMs
|
||||
stored inside the SFX WAD entry with the same ID (e.g. EDF036D6 'Music', E8F980BE 'Music_PDLC',
|
||||
57572DAC 'Global', FCA4F778).
|
||||
|
||||
BKPK header (u32 LE):
|
||||
0x00 'BKPK' 0x04 version 0x01020000 0x08 pack id (== WAD entry hash)
|
||||
0x10 media count N 0x18 table size (12N) 0x1C table offset (0x48)
|
||||
0x20 total stream size (== WAD entry size; end of last media aligned to 0x800)
|
||||
0x2C end of table (0x48 + 12N) 0x38 embedded bank size 0x3C embedded bank offset
|
||||
table: N x {u32 media id, u32 size, u32 offset-in-WAD-entry}, sorted by id; each media
|
||||
starts at the next 0x800-aligned offset after the previous one.
|
||||
See docs/wad_format.md.
|
||||
"""
|
||||
import struct
|
||||
|
||||
ALIGN = 0x800
|
||||
|
||||
|
||||
def _a(x):
|
||||
return (x + ALIGN - 1) // ALIGN * ALIGN
|
||||
|
||||
|
||||
class Pack:
|
||||
def __init__(self, bnk: bytes):
|
||||
if bnk[:4] != b"BKPK":
|
||||
raise ValueError("not a BKPK pack")
|
||||
self.raw = bnk
|
||||
self.id = struct.unpack_from("<I", bnk, 8)[0]
|
||||
n = struct.unpack_from("<I", bnk, 0x10)[0]
|
||||
self.table_off = struct.unpack_from("<I", bnk, 0x1C)[0]
|
||||
self.total = struct.unpack_from("<I", bnk, 0x20)[0]
|
||||
self.entries = [list(struct.unpack_from("<III", bnk, self.table_off + 12 * i)) for i in range(n)]
|
||||
self.index = {e[0]: i for i, e in enumerate(self.entries)}
|
||||
|
||||
def media(self, payload: bytes, mid: int) -> bytes:
|
||||
_, size, off = self.entries[self.index[mid]]
|
||||
return payload[off:off + size]
|
||||
|
||||
def gaps(self, payload: bytes) -> dict:
|
||||
"""Non-zero padding between media: {start offset: bytes}. Saved with the originals so that a
|
||||
full revert restores the pack byte-for-byte."""
|
||||
out = {}
|
||||
for i, (mid, size, off) in enumerate(self.entries):
|
||||
a = off + size
|
||||
b = self.entries[i + 1][2] if i + 1 < len(self.entries) else self.total
|
||||
g = payload[a:b]
|
||||
if g.strip(b"\0"):
|
||||
out[a] = g
|
||||
return out
|
||||
|
||||
def rebuild(self, payload: bytes, replacements: dict, gaps: dict = None):
|
||||
"""Returns (new_bnk_bytes, new_wad_payload).
|
||||
|
||||
Media are laid out by the pack rules (sorted order, 0x800-aligned, total aligned). Padding is
|
||||
taken from `gaps` (original non-zero padding) when its position matches, else from the current
|
||||
payload when that gap did not move, else zeros."""
|
||||
missing = set(replacements) - set(self.index)
|
||||
if missing:
|
||||
raise KeyError("media not in pack: %s" % ", ".join("%08X" % m for m in missing))
|
||||
gaps = gaps or {}
|
||||
cur_gaps = {}
|
||||
for i, (mid, size, off) in enumerate(self.entries):
|
||||
nxt = self.entries[i + 1][2] if i + 1 < len(self.entries) else self.total
|
||||
cur_gaps[off + size] = nxt
|
||||
|
||||
out = bytearray()
|
||||
new_entries = []
|
||||
|
||||
def pad(upto):
|
||||
a, n = len(out), upto - len(out)
|
||||
if n <= 0:
|
||||
return
|
||||
g = gaps.get(a)
|
||||
if g is not None and len(g) == n:
|
||||
out.extend(g)
|
||||
elif cur_gaps.get(a) == upto:
|
||||
out.extend(payload[a:upto])
|
||||
else:
|
||||
out.extend(bytes(n))
|
||||
|
||||
for mid, size, off in self.entries:
|
||||
pad(_a(len(out)))
|
||||
blob = replacements[mid] if mid in replacements else payload[off:off + size]
|
||||
new_entries.append((mid, len(blob), len(out)))
|
||||
out += blob
|
||||
total = _a(len(out))
|
||||
pad(total)
|
||||
bnk = bytearray(self.raw)
|
||||
struct.pack_into("<I", bnk, 0x20, total)
|
||||
for i, e in enumerate(new_entries):
|
||||
struct.pack_into("<III", bnk, self.table_off + 12 * i, *e)
|
||||
return bytes(bnk), bytes(out)
|
||||
44
ackit/paths.py
Normal file
44
ackit/paths.py
Normal file
@@ -0,0 +1,44 @@
|
||||
"""Filesystem locations: bundled resources and per-user data/config/cache folders.
|
||||
|
||||
Works from a source checkout and from a PyInstaller bundle (sys._MEIPASS). Override all user
|
||||
folders with ACKIT_HOME (used by tests).
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
|
||||
APP_NAME = "UnheardCity"
|
||||
|
||||
|
||||
def resource_root():
|
||||
"""Folder that contains `data/` (repo root, or the PyInstaller bundle root)."""
|
||||
if getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS"):
|
||||
return sys._MEIPASS
|
||||
return os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
|
||||
def package_data(*parts):
|
||||
"""Files shipped inside the package (codebooks, known setups, hash list)."""
|
||||
return os.path.join(os.path.dirname(os.path.abspath(__file__)), "data", *parts)
|
||||
|
||||
|
||||
def catalog_dir():
|
||||
return os.path.join(resource_root(), "data", "catalog")
|
||||
|
||||
|
||||
def _base(kind):
|
||||
home = os.environ.get("ACKIT_HOME")
|
||||
if home:
|
||||
return os.path.join(home, kind)
|
||||
if sys.platform == "win32":
|
||||
root = os.environ.get("LOCALAPPDATA" if kind == "cache" else "APPDATA") or os.path.expanduser("~")
|
||||
return os.path.join(root, APP_NAME, kind if kind == "cache" else "")
|
||||
xdg = {"data": ("XDG_DATA_HOME", "~/.local/share"), "config": ("XDG_CONFIG_HOME", "~/.config"),
|
||||
"cache": ("XDG_CACHE_HOME", "~/.cache")}[kind]
|
||||
return os.path.join(os.environ.get(xdg[0]) or os.path.expanduser(xdg[1]), APP_NAME.lower())
|
||||
|
||||
|
||||
def user_dir(kind):
|
||||
"""kind: 'data' (originals store, notes), 'config' (settings), 'cache' (indexes)."""
|
||||
p = os.path.normpath(_base(kind))
|
||||
os.makedirs(p, exist_ok=True)
|
||||
return p
|
||||
32
ackit/settings.py
Normal file
32
ackit/settings.py
Normal file
@@ -0,0 +1,32 @@
|
||||
"""Small JSON settings file shared by the CLI and the editor (user config folder)."""
|
||||
import json
|
||||
import os
|
||||
|
||||
from .paths import user_dir
|
||||
|
||||
DEFAULTS = {"game_dir": None, "store_root": None, "full_backup": True, "language": None,
|
||||
"input_device": None, "output_device": None, "last_dir": None,
|
||||
"background_enabled": True, "background_opacity": 0.1}
|
||||
OBSOLETE = ("theme", "background_image")
|
||||
|
||||
|
||||
def path():
|
||||
return os.path.join(user_dir("config"), "settings.json")
|
||||
|
||||
|
||||
def load():
|
||||
s = dict(DEFAULTS)
|
||||
try:
|
||||
s.update(json.load(open(path(), encoding="utf-8")))
|
||||
except (OSError, ValueError):
|
||||
pass
|
||||
for k in OBSOLETE:
|
||||
s.pop(k, None)
|
||||
return s
|
||||
|
||||
|
||||
def save(s):
|
||||
tmp = path() + ".tmp"
|
||||
with open(tmp, "w", encoding="utf-8") as f:
|
||||
json.dump(s, f, indent=1)
|
||||
os.replace(tmp, path())
|
||||
0
ackit/verify/__init__.py
Normal file
0
ackit/verify/__init__.py
Normal file
183
ackit/verify/debugger.py
Normal file
183
ackit/verify/debugger.py
Normal file
@@ -0,0 +1,183 @@
|
||||
"""Windows only. Unattended game run under cdb (WinDbg console debugger) with logging breakpoints.
|
||||
|
||||
Launch method (tested, see NOTES.md): with Steam running, BatmanAC.exe can be started directly
|
||||
with SteamAppId=200260 in the environment (no relaunch through Steam). SteamStub decrypts .text
|
||||
at runtime, so we wait until a known code signature is readable, then attach cdb non-intrusively
|
||||
except for int3 breakpoints placed on already-decrypted code.
|
||||
"""
|
||||
import ctypes
|
||||
import ctypes.wintypes as wt
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import time
|
||||
|
||||
APPID = 200260
|
||||
|
||||
|
||||
def find_cdb():
|
||||
"""x86 cdb.exe from the WinDbg Store package (its path changes on every auto-update)."""
|
||||
if os.environ.get("ACKIT_CDB"):
|
||||
return os.environ["ACKIT_CDB"]
|
||||
out = subprocess.run(["powershell", "-NoProfile", "-Command",
|
||||
"(Get-AppxPackage Microsoft.WinDbg).InstallLocation"],
|
||||
capture_output=True, text=True).stdout.strip()
|
||||
for base in out.splitlines()[::-1]:
|
||||
p = os.path.join(base.strip(), "x86", "cdb.exe")
|
||||
if os.path.exists(p):
|
||||
return p
|
||||
for p in (r"C:\Program Files (x86)\Windows Kits\10\Debuggers\x86\cdb.exe",):
|
||||
if os.path.exists(p):
|
||||
return p
|
||||
raise FileNotFoundError("x86 cdb.exe not found (install WinDbg or set ACKIT_CDB)")
|
||||
|
||||
# Runtime addresses (image base 0x400000; ASLR off for this exe). See docs/decoder_re.md.
|
||||
ADDR = dict(
|
||||
get_or_create_setup=0x010B3400, # __thiscall(cache, AkSrcInfo*, ...) ; info+0x9c hash
|
||||
parse_setup=0x010B4D10, # WwiseVorbisParseSetupHeader(info, channels, reader, pool) -> 0 ok
|
||||
parse_setup_ret=0x010B351A, # in get_or_create_setup, right after the call (eax = result)
|
||||
sig_site=0x010B347B, # mov ebx,[ebp+0x94] (decryption probe)
|
||||
)
|
||||
SIG = bytes.fromhex("8B9D94000000")
|
||||
|
||||
k32 = ctypes.WinDLL("kernel32", use_last_error=True)
|
||||
PROCESS_VM_READ = 0x0010
|
||||
PROCESS_QUERY_INFORMATION = 0x0400
|
||||
k32.OpenProcess.restype = wt.HANDLE
|
||||
k32.ReadProcessMemory.argtypes = [wt.HANDLE, wt.LPCVOID, wt.LPVOID, ctypes.c_size_t, ctypes.POINTER(ctypes.c_size_t)]
|
||||
|
||||
|
||||
def read_mem(pid, addr, n):
|
||||
h = k32.OpenProcess(PROCESS_VM_READ | PROCESS_QUERY_INFORMATION, False, pid)
|
||||
if not h:
|
||||
return None
|
||||
try:
|
||||
buf = ctypes.create_string_buffer(n)
|
||||
got = ctypes.c_size_t()
|
||||
if not k32.ReadProcessMemory(h, ctypes.c_void_p(addr), buf, n, ctypes.byref(got)):
|
||||
return None
|
||||
return buf.raw[:got.value]
|
||||
finally:
|
||||
k32.CloseHandle(h)
|
||||
|
||||
|
||||
u32 = ctypes.WinDLL("user32", use_last_error=True)
|
||||
WM_KEYDOWN, WM_KEYUP = 0x0100, 0x0101
|
||||
VK = dict(enter=0x0D, space=0x20, esc=0x1B, up=0x26, down=0x28, left=0x25, right=0x27, e=0x45)
|
||||
|
||||
|
||||
def game_windows(pid):
|
||||
"""Top-level windows of the game process."""
|
||||
found = []
|
||||
cb_t = ctypes.WINFUNCTYPE(wt.BOOL, wt.HWND, wt.LPARAM)
|
||||
|
||||
def cb(hwnd, _):
|
||||
p = wt.DWORD()
|
||||
u32.GetWindowThreadProcessId(hwnd, ctypes.byref(p))
|
||||
if p.value == pid and u32.IsWindowVisible(hwnd):
|
||||
found.append(hwnd)
|
||||
return True
|
||||
u32.EnumWindows(cb_t(cb), 0)
|
||||
return found
|
||||
|
||||
|
||||
def post_key(pid, key, hold=0.08):
|
||||
"""Post a key press to the game's window only (never global SendInput)."""
|
||||
vk = VK[key] if isinstance(key, str) else key
|
||||
sc = u32.MapVirtualKeyW(vk, 0)
|
||||
n = 0
|
||||
for hwnd in game_windows(pid):
|
||||
u32.PostMessageW(hwnd, WM_KEYDOWN, vk, 1 | (sc << 16))
|
||||
time.sleep(hold)
|
||||
u32.PostMessageW(hwnd, WM_KEYUP, vk, 1 | (sc << 16) | (3 << 30))
|
||||
n += 1
|
||||
return n
|
||||
|
||||
|
||||
def running_game_pids():
|
||||
out = subprocess.run(["tasklist", "/FI", "IMAGENAME eq BatmanAC.exe", "/FO", "CSV", "/NH"],
|
||||
capture_output=True, text=True).stdout
|
||||
return [int(m.group(1)) for m in re.finditer(r'"BatmanAC\.exe","(\d+)"', out)]
|
||||
|
||||
|
||||
def kill_game():
|
||||
for pid in running_game_pids():
|
||||
subprocess.run(["taskkill", "/F", "/PID", str(pid)], capture_output=True)
|
||||
|
||||
|
||||
def launch_game(exe, extra_args=()):
|
||||
"""Start BatmanAC.exe directly (Steam must be running; SteamAppId makes steam_api accept it)."""
|
||||
env = dict(os.environ, SteamAppId=str(APPID), SteamGameId=str(APPID))
|
||||
return subprocess.Popen([exe] + list(extra_args), cwd=os.path.dirname(exe), env=env)
|
||||
|
||||
|
||||
def wait_decrypted(pid, timeout=60):
|
||||
t0 = time.time()
|
||||
while time.time() - t0 < timeout:
|
||||
if read_mem(pid, ADDR["sig_site"], len(SIG)) == SIG:
|
||||
return time.time() - t0
|
||||
time.sleep(0.05)
|
||||
raise TimeoutError("code never decrypted (signature not found)")
|
||||
|
||||
|
||||
def cdb_script(extra_bps="", dump_info=False):
|
||||
"""Debugger command file: log codec setup creation + parse result; log 2nd-chance crashes."""
|
||||
a = ADDR
|
||||
dump = "dd poi(@esp+4) L0x30; " if dump_info else ""
|
||||
return "\n".join([
|
||||
".echotime",
|
||||
"sxd av",
|
||||
'sxd -c2 ".echo ACKIT_CRASH; .echotime; r; kv 30; .ecxr; q" av',
|
||||
"sxn ch", "sxn sse", "sxn eh", "sxn clr", "sxn ld", "sxn ud",
|
||||
"sxi ibp",
|
||||
# stream codec creation: log the AkVorbisInfo fields of the stream being opened
|
||||
'bp %#x ".printf \\"ACKIT_SETUP hash=%%08x frames=%%u alloc=%%u ch=%%u rate=%%u\\\\n\\", '
|
||||
'poi(poi(@esp+4)+0x9c), poi(poi(@esp+4)+0x78), poi(poi(@esp+4)+0x94), '
|
||||
'wo(poi(@esp+4)+0x5a), poi(poi(@esp+4)+0x5c); %sgc"' % (a["get_or_create_setup"], dump),
|
||||
# setup parse result (0 = OK)
|
||||
'bp %#x ".printf \\"ACKIT_PARSE ret=%%x\\\\n\\", @eax; gc"' % a["parse_setup_ret"],
|
||||
extra_bps,
|
||||
".echo ACKIT_ATTACHED",
|
||||
"g",
|
||||
])
|
||||
|
||||
|
||||
class Run:
|
||||
def __init__(self, exe, log_path, script_extra="", dump_info=False):
|
||||
self.exe = exe
|
||||
self.log_path = log_path
|
||||
self.script_extra = script_extra
|
||||
self.dump_info = dump_info
|
||||
self.game = None
|
||||
self.dbg = None
|
||||
|
||||
def start(self):
|
||||
kill_game()
|
||||
self.t0 = time.time()
|
||||
self.game = launch_game(self.exe)
|
||||
pid = self.game.pid
|
||||
dt = wait_decrypted(pid)
|
||||
script = self.log_path + ".cdbcmds"
|
||||
open(script, "w").write(cdb_script(self.script_extra, self.dump_info))
|
||||
self.dbg = subprocess.Popen([find_cdb(), "-p", str(pid), "-pd", "-logo", self.log_path,
|
||||
"-y", "", "-cf", script],
|
||||
stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL)
|
||||
return dict(pid=pid, decrypt_s=dt)
|
||||
|
||||
def alive(self):
|
||||
return self.game.poll() is None
|
||||
|
||||
def stop(self):
|
||||
kill_game()
|
||||
if self.dbg:
|
||||
try:
|
||||
self.dbg.wait(15)
|
||||
except subprocess.TimeoutExpired:
|
||||
self.dbg.kill()
|
||||
|
||||
def log(self):
|
||||
try:
|
||||
return open(self.log_path, encoding="utf-8", errors="replace").read()
|
||||
except FileNotFoundError:
|
||||
return ""
|
||||
121
ackit/verify/ingame.py
Normal file
121
ackit/verify/ingame.py
Normal file
@@ -0,0 +1,121 @@
|
||||
"""Automated in-game test (Windows): does a replaced sound actually play?
|
||||
|
||||
Flow: apply the replacement through `Workspace.apply` (the same code path as the editor) ->
|
||||
start the game under cdb with logging breakpoints on the decoder + record the speakers (WASAPI
|
||||
loopback) -> check: no crash, the codec setup of the injected WEM was created and parsed OK, and the
|
||||
injected audio is heard -> kill the game -> revert and check the files are byte-identical again.
|
||||
|
||||
Default target: SFX music EDF036D6/393BC53A (intro/title music), which the game starts ~5 s after
|
||||
launch without input. That slot ignores sources shorter than ~10 s, so probes are 30 s long.
|
||||
Only correlation scores of the recording are kept (the loopback may contain other apps' audio).
|
||||
"""
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import time
|
||||
|
||||
import numpy as np
|
||||
|
||||
from .. import audio
|
||||
from ..originals import sha256
|
||||
|
||||
DEFAULT_TARGET = "SFX:EDF036D6/393BC53A"
|
||||
RATE = 48000
|
||||
|
||||
|
||||
def probe_signal(seconds=30.0, rate=RATE, seed=1234):
|
||||
"""Pink-ish noise + vibrato tone: easy to recognise in a recording."""
|
||||
rng = np.random.default_rng(seed)
|
||||
n = int(seconds * rate)
|
||||
white = rng.standard_normal(n)
|
||||
pink = np.cumsum(white) * 0.002
|
||||
pink -= np.convolve(pink, np.ones(2048) / 2048, mode="same")
|
||||
t = np.arange(n) / rate
|
||||
tone = 0.25 * np.sin(2 * np.pi * (440 + 20 * np.sin(2 * np.pi * 0.5 * t)) * t)
|
||||
x = (pink / (np.max(np.abs(pink)) + 1e-9) * 0.35 + tone).astype(np.float32)
|
||||
return x[:, None], rate
|
||||
|
||||
|
||||
def _mono48(wem):
|
||||
x, sr = audio.decode_wem(wem)
|
||||
return audio.to_channels(audio.resample(x, sr, RATE), 1)[:, 0]
|
||||
|
||||
|
||||
def _sha1_file(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 run(ws, key=DEFAULT_TARGET, wem=None, mode="modded", seconds=40.0, threshold=0.5,
|
||||
logdir=None, revert_after=True, log=print):
|
||||
"""mode: 'modded' (apply `wem`, test, revert), 'baseline' (nothing), 'installed' (test the
|
||||
current state). Returns a report dict with report['result'] in ('PASS', 'FAIL')."""
|
||||
from .debugger import Run, kill_game
|
||||
from .loopback import LoopbackRecorder, detect_chunks
|
||||
from ..inject import pack_bnk_path, parse_target
|
||||
|
||||
s = ws.catalog.resolve(key)
|
||||
wad = ws.wad_path(s)
|
||||
files = [wad] + ([pack_bnk_path(wad, parse_target(s.target)[0])] if s.is_pack_media else [])
|
||||
logdir = logdir or os.path.join(os.getcwd(), "logs")
|
||||
os.makedirs(logdir, exist_ok=True)
|
||||
stamp = time.strftime("%Y%m%d_%H%M%S")
|
||||
logp = os.path.join(logdir, "ingame_%s.log" % stamp)
|
||||
kill_game()
|
||||
report = dict(target=s.key, name=s.name, mode=mode)
|
||||
before = {f: _sha1_file(f) for f in files} if mode == "modded" else {}
|
||||
|
||||
if mode == "modded":
|
||||
ws.apply([(s, wem, {"source": "ingame-test"})], progress=log)
|
||||
expect = ws.current_bytes(s)
|
||||
orig = ws.original_bytes(s)
|
||||
from ..wem import Wem
|
||||
report["expect_hash"] = "%08x" % Wem.parse(expect).vorb["codebook_hash"]
|
||||
ref_expect, ref_orig = _mono48(expect), _mono48(orig)
|
||||
|
||||
rec = LoopbackRecorder(seconds + 10)
|
||||
rec.start()
|
||||
runner = Run(ws.game.exe, logp)
|
||||
try:
|
||||
report.update(runner.start())
|
||||
while time.time() - runner.t0 < seconds and runner.alive():
|
||||
time.sleep(0.5)
|
||||
if re.search(r"^ACKIT_CRASH", runner.log(), re.M):
|
||||
break
|
||||
report["alive_at_end"] = runner.alive()
|
||||
finally:
|
||||
runner.stop()
|
||||
rec.stop_flag = True
|
||||
rec.join(5)
|
||||
if mode == "modded" and revert_after:
|
||||
ws.revert([s], progress=log)
|
||||
report["reverted_byte_identical"] = all(_sha1_file(f) == h for f, h in before.items())
|
||||
|
||||
text = runner.log()
|
||||
report["crash"] = bool(re.search(r"^ACKIT_CRASH", text, re.M))
|
||||
ev = []
|
||||
for ln in text.splitlines():
|
||||
m = re.match(r"ACKIT_SETUP hash=([0-9a-f]+)", ln)
|
||||
if m:
|
||||
ev.append([m.group(1), "cached"])
|
||||
m = re.match(r"ACKIT_PARSE ret=([0-9a-f]+)", ln)
|
||||
if m and ev and ev[-1][1] == "cached":
|
||||
ev[-1][1] = m.group(1)
|
||||
report["setups"] = ["%s:%s" % tuple(e) for e in ev]
|
||||
mine = [r for h, r in ev if h == report["expect_hash"]]
|
||||
report["setup_ok"] = bool(mine) and all(r in ("0", "cached") for r in mine)
|
||||
cap = rec.audio()
|
||||
report["capture_error"] = rec.error
|
||||
report["ncc_expected"] = round(detect_chunks(cap, ref_expect)[0], 3)
|
||||
report["ncc_original"] = round(detect_chunks(cap, ref_orig)[0], 3)
|
||||
ok = (not report["crash"] and report["alive_at_end"] and report["setup_ok"]
|
||||
and report["ncc_expected"] >= threshold
|
||||
and (mode == "baseline" or sha256(expect) == sha256(orig) or report["ncc_expected"] > report["ncc_original"])
|
||||
and report.get("reverted_byte_identical", True))
|
||||
report["result"] = "PASS" if ok else "FAIL"
|
||||
json.dump(report, open(logp.replace(".log", ".json"), "w"), indent=1)
|
||||
return report
|
||||
87
ackit/verify/loopback.py
Normal file
87
ackit/verify/loopback.py
Normal file
@@ -0,0 +1,87 @@
|
||||
"""WASAPI loopback capture of the default speaker + detection of a reference sound in it."""
|
||||
import threading
|
||||
import warnings
|
||||
|
||||
import numpy as np
|
||||
|
||||
RATE = 48000
|
||||
|
||||
|
||||
class LoopbackRecorder(threading.Thread):
|
||||
def __init__(self, seconds, rate=RATE):
|
||||
super().__init__(daemon=True)
|
||||
self.seconds = seconds
|
||||
self.rate = rate
|
||||
self.chunks = []
|
||||
self.stop_flag = False
|
||||
self.error = None
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
import soundcard as sc
|
||||
sp = sc.default_speaker()
|
||||
mic = sc.get_microphone(id=str(sp.name), include_loopback=True)
|
||||
block = self.rate // 10
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("ignore") # "data discontinuity" warnings
|
||||
with mic.recorder(samplerate=self.rate, blocksize=block) as rec:
|
||||
n = int(self.seconds * 10)
|
||||
for _ in range(n):
|
||||
if self.stop_flag:
|
||||
break
|
||||
self.chunks.append(rec.record(numframes=block))
|
||||
except Exception as e: # noqa
|
||||
self.error = repr(e)
|
||||
|
||||
def audio(self):
|
||||
if not self.chunks:
|
||||
return np.zeros(0, dtype=np.float32)
|
||||
a = np.concatenate(self.chunks, axis=0)
|
||||
return a.mean(axis=1).astype(np.float32) if a.ndim == 2 else a.astype(np.float32)
|
||||
|
||||
|
||||
def resample(x, src_rate, dst_rate):
|
||||
if src_rate == dst_rate:
|
||||
return x.astype(np.float32)
|
||||
n = int(round(len(x) * dst_rate / src_rate))
|
||||
t = np.linspace(0, len(x) - 1, n)
|
||||
return np.interp(t, np.arange(len(x)), x).astype(np.float32)
|
||||
|
||||
|
||||
def detect(capture, ref, rate=RATE):
|
||||
"""Max normalized cross-correlation of ref against every same-length window of capture.
|
||||
Returns (score 0..1, time_offset_seconds, ref_rms_in_window)."""
|
||||
if len(capture) < len(ref) or len(ref) == 0:
|
||||
return 0.0, None, 0.0
|
||||
ref = ref - ref.mean()
|
||||
rn = np.linalg.norm(ref)
|
||||
if rn == 0:
|
||||
return 0.0, None, 0.0
|
||||
n = 1 << int(np.ceil(np.log2(len(capture) + len(ref))))
|
||||
xc = np.fft.irfft(np.fft.rfft(capture, n) * np.conj(np.fft.rfft(ref, n)), n)[:len(capture) - len(ref) + 1]
|
||||
c2 = np.concatenate([[0.0], np.cumsum(capture.astype(np.float64) ** 2)])
|
||||
win = np.sqrt(np.maximum(c2[len(ref):] - c2[:-len(ref)], 1e-12))
|
||||
ncc = xc / (rn * win)
|
||||
# ignore near-silent windows
|
||||
ncc[win < 1e-3 * np.sqrt(len(ref))] = 0
|
||||
i = int(np.argmax(np.abs(ncc)))
|
||||
return float(abs(ncc[i])), i / rate, float(win[i] / np.sqrt(len(ref)))
|
||||
|
||||
|
||||
def detect_chunks(capture, ref, rate=RATE, chunk_s=1.0, step_s=0.5):
|
||||
"""Best NCC over 1 s chunks of the reference (robust to partial playback and resampler drift).
|
||||
Returns (best score, chunk start in ref [s], position in capture [s])."""
|
||||
c = int(chunk_s * rate)
|
||||
s = int(step_s * rate)
|
||||
best = (0.0, None, None)
|
||||
if len(ref) <= c:
|
||||
sc, t, _ = detect(capture, ref, rate)
|
||||
return sc, 0.0, t
|
||||
for i in range(0, len(ref) - c + 1, s):
|
||||
seg = ref[i:i + c]
|
||||
if np.sqrt(np.mean(seg ** 2)) < 1e-3:
|
||||
continue
|
||||
sc, t, _ = detect(capture, seg, rate)
|
||||
if sc > best[0]:
|
||||
best = (sc, i / rate, t)
|
||||
return best
|
||||
93
ackit/verify/selfcheck.py
Normal file
93
ackit/verify/selfcheck.py
Normal file
@@ -0,0 +1,93 @@
|
||||
"""Offline self-check against the installed game (no game launch, nothing is modified).
|
||||
|
||||
1. every audio archive parses and follows the WAD layout rules;
|
||||
2. a sample of game WEMs is rebuilt byte-for-byte from the derived format rules;
|
||||
3. a test signal is encoded for a speech and a music sound and decoded again (correlation check);
|
||||
4. the originals store is consistent (blobs intact, recorded states).
|
||||
"""
|
||||
import random
|
||||
|
||||
import numpy as np
|
||||
|
||||
from .. import audio
|
||||
from ..builder import build_wem, setup_header_values
|
||||
from ..convert import library
|
||||
from ..inject import is_pack
|
||||
from ..wad import Wad
|
||||
from ..wem import Wem, split_concatenated
|
||||
from ..wwvorbis import transcode_setup, W2V
|
||||
|
||||
|
||||
def rebuild_identical(r):
|
||||
"""True if a game WEM is reproduced byte-for-byte by the builder."""
|
||||
m = Wem.parse(r)
|
||||
v = m.vorb
|
||||
s = m.setup_packet()
|
||||
_, info = transcode_setup(s, W2V, m.channels, library())
|
||||
pk, _ = m.audio_packets()
|
||||
mb = info.mode_bits
|
||||
if v["bs0_exp"] != v["bs1_exp"]:
|
||||
flags = [info.mode_blockflags[p[0] & ((1 << mb) - 1)] if mb else info.mode_blockflags[0] for _, p in pk]
|
||||
else:
|
||||
flags = [info.mode_blockflags[(p[0] >> 1) & ((1 << mb) - 1)] if mb else info.mode_blockflags[0] for _, p in pk]
|
||||
hv = setup_header_values(s, m.channels)
|
||||
extra = [c for c in m.chunks if c.tag not in (b"fmt ", b"data")]
|
||||
for interval in (8192, 1024, 2048, 4096, 16384, 32768, 512, 256):
|
||||
out = build_wem(m.channels, m.rate, v["bs0_exp"], v["bs1_exp"], s, [p for _, p in pk], flags,
|
||||
v["total_frames"], hv["hash"], hv["alloc"], hv["alloc64"], m.channel_mask,
|
||||
seek=v["seek_table_size"] > 0, seek_interval=interval, valid_bits=m.valid_bits,
|
||||
extra_chunks=extra)
|
||||
if out == r:
|
||||
return True
|
||||
if v["seek_table_size"] == 0:
|
||||
break
|
||||
return False
|
||||
|
||||
|
||||
def run(ws, sample=300, log=print):
|
||||
ok = True
|
||||
# 1 + 2
|
||||
for arch, path in ws.game.archives.items():
|
||||
w = Wad(path)
|
||||
probs = w.check()
|
||||
rnd = random.Random(1)
|
||||
ents = rnd.sample(w.entries, min(len(w.entries), 60))
|
||||
n = good = 0
|
||||
for e in ents:
|
||||
if is_pack(path, e.hash): # stream packs are checked by the tests (they are large)
|
||||
continue
|
||||
for _, r in split_concatenated(w.read(e.hash))[0][:5]:
|
||||
n += 1
|
||||
try:
|
||||
good += rebuild_identical(r)
|
||||
except Exception:
|
||||
pass
|
||||
if n >= sample // max(1, len(ws.game.archives)):
|
||||
break
|
||||
log("%-4s layout %s, rebuilt %d/%d sample WEMs byte-identical" % (arch, "OK" if not probs else probs[:2], good, n))
|
||||
ok &= not probs and good >= n - 2 # 4 looping music tracks are known exceptions
|
||||
# 3
|
||||
t = np.arange(48000 * 2) / 48000.0
|
||||
sig = (0.3 * np.sin(2 * np.pi * 330 * t) * np.sin(2 * np.pi * 1.5 * t)).astype(np.float32)[:, None]
|
||||
for s in [x for x in ws.catalog.sounds if x.group == "Speech"][:1] + \
|
||||
[x for x in ws.catalog.sounds if x.group == "Music"][:1]:
|
||||
wem, info = ws.prepare(s, sig, 48000)
|
||||
y, sr = audio.decode_wem(wem)
|
||||
ref = audio.to_channels(audio.resample(sig, 48000, sr), y.shape[1])
|
||||
n = min(len(ref), len(y))
|
||||
c = float(np.corrcoef(ref[:n, 0], y[:n, 0])[0, 1])
|
||||
log("encode/decode %-30s %d Hz q%.1f corr %.3f" % (s.name[:30], info["rate"], info["quality"], c))
|
||||
ok &= c > 0.95
|
||||
# 4
|
||||
bad = 0
|
||||
for k, e in ws.store.entries.items():
|
||||
try:
|
||||
ws.store.get_blob(e["orig"])
|
||||
if e.get("applied"):
|
||||
ws.store.get_blob(e["applied"])
|
||||
except (OSError, IOError):
|
||||
bad += 1
|
||||
log("originals store: %d record(s), %d damaged" % (len(ws.store.entries), bad))
|
||||
ok &= bad == 0
|
||||
log("SELF-CHECK %s" % ("PASS" if ok else "FAIL"))
|
||||
return ok
|
||||
122
ackit/wad.py
Normal file
122
ackit/wad.py
Normal file
@@ -0,0 +1,122 @@
|
||||
"""Arkham City 'WWAD' container (SFX and *_LOC_*.wad).
|
||||
|
||||
Layout (little endian):
|
||||
0x00 'WWAD'
|
||||
0x04 u32 version (0x01020000)
|
||||
0x08 u32 entry count N
|
||||
0x0C u32 table offset (0x1C)
|
||||
0x10 u32 table size (N*12)
|
||||
0x14 u32 data start (header area size, 0x1000 or 0x8000; aligned)
|
||||
0x18 u32 data size (file size - data start)
|
||||
0x1C N x {u32 hash, u32 size, u32 offset} sorted by hash as *signed* int32
|
||||
Entries are stored in table order, each at a 0x1000-aligned offset; 'size' is the
|
||||
entry payload size padded to 0x800. Padding bytes are zero; the file ends 0x1000-aligned.
|
||||
See docs/wad_format.md.
|
||||
"""
|
||||
import struct
|
||||
from dataclasses import dataclass
|
||||
|
||||
MAGIC = b"WWAD"
|
||||
ENTRY_ALIGN = 0x1000
|
||||
SIZE_ALIGN = 0x800
|
||||
|
||||
|
||||
def _s32(x):
|
||||
return struct.unpack("<i", struct.pack("<I", x))[0]
|
||||
|
||||
|
||||
def align(x, a):
|
||||
return (x + a - 1) // a * a
|
||||
|
||||
|
||||
@dataclass
|
||||
class Entry:
|
||||
hash: int
|
||||
size: int
|
||||
offset: int
|
||||
|
||||
|
||||
class Wad:
|
||||
def __init__(self, path):
|
||||
self.path = path
|
||||
with open(path, "rb") as f:
|
||||
hdr = f.read(0x1C)
|
||||
(magic, self.version, n, self.table_off, self.table_size,
|
||||
self.data_start, self.data_size) = struct.unpack("<4s6I", hdr)
|
||||
if magic != MAGIC:
|
||||
raise ValueError("not a WWAD file")
|
||||
f.seek(self.table_off)
|
||||
tbl = f.read(12 * n)
|
||||
f.seek(0)
|
||||
self.header_area = f.read(self.data_start)
|
||||
self.entries = [Entry(*struct.unpack_from("<III", tbl, 12 * i)) for i in range(n)]
|
||||
self.index = {e.hash: e for e in self.entries}
|
||||
|
||||
def read(self, h) -> bytes:
|
||||
e = self.index[h]
|
||||
with open(self.path, "rb") as f:
|
||||
f.seek(e.offset)
|
||||
return f.read(e.size)
|
||||
|
||||
def iter_entries(self):
|
||||
with open(self.path, "rb") as f:
|
||||
for e in self.entries:
|
||||
f.seek(e.offset)
|
||||
yield e, f.read(e.size)
|
||||
|
||||
def check(self):
|
||||
"""Validate structural invariants; returns list of problems (empty = OK)."""
|
||||
probs = []
|
||||
hs = [_s32(e.hash) for e in self.entries]
|
||||
if hs != sorted(hs):
|
||||
probs.append("table not sorted by signed hash")
|
||||
if self.table_size != 12 * len(self.entries):
|
||||
probs.append("table size mismatch")
|
||||
pos = self.data_start
|
||||
for e in self.entries:
|
||||
if e.offset != align(pos, ENTRY_ALIGN):
|
||||
probs.append("entry %08X at %x, expected %x" % (e.hash, e.offset, align(pos, ENTRY_ALIGN)))
|
||||
if e.size % SIZE_ALIGN:
|
||||
probs.append("entry %08X size not 0x800-aligned" % e.hash)
|
||||
pos = e.offset + e.size
|
||||
return probs
|
||||
|
||||
|
||||
def write_wad(src: Wad, out_path: str, replacements: dict):
|
||||
"""Rebuild a WAD, replacing entry payloads (hash -> bytes). Layout rules are reproduced
|
||||
exactly so an unmodified rebuild is byte-identical to the original."""
|
||||
n = len(src.entries)
|
||||
payloads = {}
|
||||
for h, data in replacements.items():
|
||||
if h not in src.index:
|
||||
raise KeyError("hash %08X not in WAD" % h)
|
||||
payloads[h] = data + b"\0" * (align(len(data), SIZE_ALIGN) - len(data))
|
||||
new_entries = []
|
||||
pos = src.data_start
|
||||
for e in src.entries:
|
||||
size = len(payloads[e.hash]) if e.hash in payloads else e.size
|
||||
off = align(pos, ENTRY_ALIGN)
|
||||
new_entries.append(Entry(e.hash, size, off))
|
||||
pos = off + size
|
||||
end = pos
|
||||
total = align(end, ENTRY_ALIGN)
|
||||
hdr = bytearray(src.header_area)
|
||||
struct.pack_into("<4s6I", hdr, 0, MAGIC, src.version, n, src.table_off, 12 * n, src.data_start,
|
||||
total - src.data_start)
|
||||
for i, e in enumerate(new_entries):
|
||||
struct.pack_into("<III", hdr, src.table_off + 12 * i, e.hash, e.size, e.offset)
|
||||
with open(src.path, "rb") as fi, open(out_path, "wb") as fo:
|
||||
fo.write(hdr)
|
||||
for old, new in zip(src.entries, new_entries):
|
||||
fo.write(b"\0" * (new.offset - fo.tell()))
|
||||
if old.hash in payloads:
|
||||
fo.write(payloads[old.hash])
|
||||
else:
|
||||
fi.seek(old.offset)
|
||||
remaining = old.size
|
||||
while remaining:
|
||||
chunk = fi.read(min(remaining, 1 << 22))
|
||||
fo.write(chunk)
|
||||
remaining -= len(chunk)
|
||||
fo.write(b"\0" * (total - fo.tell()))
|
||||
return new_entries
|
||||
133
ackit/wem.py
Normal file
133
ackit/wem.py
Normal file
@@ -0,0 +1,133 @@
|
||||
"""Wwise RIFF (.wem) container for Arkham City (Wwise 2011.1, fmt size 0x42, Vorbis 0xFFFF)."""
|
||||
import re
|
||||
import struct
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
VORB_FMT = "<IIIHHIIHHIII2B" # AkVorbisInfo as stored at fmt+0x18 (42 bytes)
|
||||
VORB_FIELDS = ["total_frames", "loop_start_pkt", "loop_end_pkt", "loop_begin_extra", "loop_end_extra",
|
||||
"seek_table_size", "vorbis_data_offset", "max_packet_size", "last_granule_extra",
|
||||
"decode_alloc", "decode_alloc_x64", "codebook_hash", "bs0_exp", "bs1_exp"]
|
||||
|
||||
|
||||
@dataclass
|
||||
class Chunk:
|
||||
tag: bytes
|
||||
data: bytes
|
||||
|
||||
|
||||
@dataclass
|
||||
class Wem:
|
||||
chunks: list
|
||||
channels: int = 0
|
||||
rate: int = 0
|
||||
avg_bps: int = 0
|
||||
valid_bits: int = 0
|
||||
channel_mask: int = 0
|
||||
vorb: dict = field(default_factory=dict)
|
||||
|
||||
# ---- parsing ----
|
||||
@classmethod
|
||||
def parse(cls, b: bytes) -> "Wem":
|
||||
if b[:4] != b"RIFF" or b[8:12] != b"WAVE":
|
||||
raise ValueError("not RIFF/WAVE")
|
||||
riff_end = 8 + struct.unpack_from("<I", b, 4)[0]
|
||||
pos = 12
|
||||
chunks = []
|
||||
while pos + 8 <= min(riff_end, len(b)):
|
||||
tag, ln = struct.unpack_from("<4sI", b, pos)
|
||||
chunks.append(Chunk(tag, b[pos + 8:pos + 8 + ln]))
|
||||
pos += 8 + ln + (ln & 1)
|
||||
w = cls(chunks)
|
||||
fmt = w.get(b"fmt ")
|
||||
if fmt is None or len(fmt) != 0x42:
|
||||
raise ValueError("not a Wwise Vorbis WEM (fmt size %s)" % (None if fmt is None else hex(len(fmt))))
|
||||
fmt_tag, w.channels, w.rate, w.avg_bps, balign, bps, cb = struct.unpack_from("<HHIIHHH", fmt, 0)
|
||||
if fmt_tag != 0xFFFF or len(fmt) != 0x42 or cb != 0x30:
|
||||
raise ValueError("unsupported fmt (tag=%x size=%x)" % (fmt_tag, len(fmt)))
|
||||
w.valid_bits, w.channel_mask = struct.unpack_from("<HI", fmt, 0x12)
|
||||
w.vorb = dict(zip(VORB_FIELDS, struct.unpack_from(VORB_FMT, fmt, 0x18)))
|
||||
return w
|
||||
|
||||
def get(self, tag: bytes):
|
||||
for c in self.chunks:
|
||||
if c.tag == tag:
|
||||
return c.data
|
||||
return None
|
||||
|
||||
def set(self, tag: bytes, data: bytes):
|
||||
for c in self.chunks:
|
||||
if c.tag == tag:
|
||||
c.data = data
|
||||
return
|
||||
self.chunks.append(Chunk(tag, data))
|
||||
|
||||
# ---- data chunk layout ----
|
||||
@property
|
||||
def data(self) -> bytes:
|
||||
return self.get(b"data")
|
||||
|
||||
def seek_table(self):
|
||||
n = self.vorb["seek_table_size"] // 4
|
||||
return [struct.unpack_from("<HH", self.data, 4 * i) for i in range(n)]
|
||||
|
||||
def setup_packet(self) -> bytes:
|
||||
d = self.data
|
||||
off = self.vorb["seek_table_size"]
|
||||
ln = struct.unpack_from("<H", d, off)[0]
|
||||
return d[off + 2: off + 2 + ln]
|
||||
|
||||
def audio_packets(self):
|
||||
"""List of (offset_in_data, payload) for audio packets (2-byte size headers)."""
|
||||
d = self.data
|
||||
pos = self.vorb["vorbis_data_offset"]
|
||||
out = []
|
||||
while pos + 2 <= len(d):
|
||||
ln = struct.unpack_from("<H", d, pos)[0]
|
||||
if ln == 0 or pos + 2 + ln > len(d):
|
||||
break
|
||||
out.append((pos, d[pos + 2: pos + 2 + ln]))
|
||||
pos += 2 + ln
|
||||
return out, pos
|
||||
|
||||
# ---- serialization ----
|
||||
def build_fmt(self) -> bytes:
|
||||
v = self.vorb
|
||||
return (struct.pack("<HHIIHHH", 0xFFFF, self.channels, self.rate, self.avg_bps, 0, 0, 0x30)
|
||||
+ struct.pack("<HI", self.valid_bits, self.channel_mask)
|
||||
+ struct.pack(VORB_FMT, *[v[k] for k in VORB_FIELDS]))
|
||||
|
||||
def to_bytes(self) -> bytes:
|
||||
self.set(b"fmt ", self.build_fmt())
|
||||
body = b"WAVE"
|
||||
for c in self.chunks:
|
||||
# the game's WEMs never pad odd-sized chunks (all chunks before 'data' are even)
|
||||
body += struct.pack("<4sI", c.tag, len(c.data)) + c.data
|
||||
return b"RIFF" + struct.pack("<I", len(body)) + body
|
||||
|
||||
|
||||
SUB_ALIGN = 0x800
|
||||
_NONZERO = re.compile(rb"[^\x00]")
|
||||
|
||||
|
||||
def split_concatenated(b: bytes):
|
||||
"""All WEMs of a WAD entry, as [(offset, riff_bytes)], plus the end of the last one.
|
||||
|
||||
LOC entries group variations of a line (e.g. CHR_RadioScanner = 63 WEMs): each WEM starts at the
|
||||
next 0x800 boundary after the previous one (zero padding); a few entries pack them back to back."""
|
||||
out = []
|
||||
pos = 0
|
||||
end = 0
|
||||
while pos + 12 <= len(b) and b[pos:pos + 4] == b"RIFF" and b[pos + 8:pos + 12] == b"WAVE":
|
||||
end = pos + 8 + struct.unpack_from("<I", b, pos + 4)[0]
|
||||
out.append((pos, b[pos:end]))
|
||||
nxt = (end + SUB_ALIGN - 1) // SUB_ALIGN * SUB_ALIGN
|
||||
if b[end:end + 4] == b"RIFF":
|
||||
pos = end
|
||||
elif b[nxt:nxt + 4] == b"RIFF":
|
||||
pos = nxt
|
||||
else: # zero padding of arbitrary length (e.g. after an in-place replacement)
|
||||
m = _NONZERO.search(b, end)
|
||||
if not m:
|
||||
break
|
||||
pos = m.start()
|
||||
return out, end
|
||||
197
ackit/workspace.py
Normal file
197
ackit/workspace.py
Normal file
@@ -0,0 +1,197 @@
|
||||
"""High-level operations shared by the CLI and the editor: apply / revert sounds safely.
|
||||
|
||||
Safety rules (docs/decisions.md):
|
||||
* before the first change to a sound its original bytes go to the originals store;
|
||||
* optionally a full copy of each WAD (and pack index) is taken once, as a fallback;
|
||||
* each WAD is rewritten atomically; several sounds of one WAD are written in one pass;
|
||||
* if a sound in the game is neither its original nor what we applied (Steam update, "verify
|
||||
integrity", another tool), we refuse to write it unless forced, and report it.
|
||||
"""
|
||||
import os
|
||||
from collections import defaultdict
|
||||
|
||||
from . import audio
|
||||
from .catalog import Catalog
|
||||
from .encoder import encode_fit
|
||||
from .game import Game, GameError
|
||||
from .inject import inject, read_target, parse_target, pack_bnk_path
|
||||
from .originals import OriginalsStore, sha256, sha1_16
|
||||
from .pack import Pack
|
||||
from .paths import user_dir
|
||||
from .wad import Wad
|
||||
|
||||
# sound states
|
||||
ORIGINAL = "original" # untouched
|
||||
MODIFIED = "modified" # contains what we applied
|
||||
LOST = "lost" # we applied something, but the game has the original again (e.g. Steam verify)
|
||||
CHANGED = "changed" # neither original nor ours: changed underneath (update / other tool)
|
||||
FOREIGN = "foreign" # never touched by us, but differs from the catalog's original checksum
|
||||
|
||||
|
||||
class ConflictError(GameError):
|
||||
def __init__(self, sounds):
|
||||
self.sounds = sounds
|
||||
super().__init__("These sounds were changed outside this tool (Steam update or 'verify', or "
|
||||
"another mod tool): %s.\nRevert them or confirm to overwrite."
|
||||
% ", ".join(s.name for s in sounds[:5]) + (" ..." if len(sounds) > 5 else ""))
|
||||
|
||||
|
||||
def default_store_root(game):
|
||||
return os.path.join(user_dir("data"), "stores", game.store_id)
|
||||
|
||||
|
||||
class Workspace:
|
||||
def __init__(self, game_dir, store_root=None, archives=None, full_backup=True, progress=None):
|
||||
self.game = game_dir if isinstance(game_dir, Game) else Game(game_dir)
|
||||
self.store = OriginalsStore(store_root or default_store_root(self.game), self.game.root)
|
||||
self.catalog = Catalog(self.game, archives, progress)
|
||||
self.full_backup = full_backup
|
||||
|
||||
# reading -----------------------------------------------------------------------------------
|
||||
def sound(self, name_or_key):
|
||||
return self.catalog.resolve(name_or_key)
|
||||
|
||||
def wad_path(self, s):
|
||||
return self.game.wad(s.archive)
|
||||
|
||||
def current_bytes(self, s):
|
||||
return read_target(self.wad_path(s), s.target)
|
||||
|
||||
def original_bytes(self, s):
|
||||
return self.store.original(s.key) or self.current_bytes(s)
|
||||
|
||||
def state(self, s, current=None):
|
||||
cur = current if current is not None else self.current_bytes(s)
|
||||
e = self.store.get(s.key)
|
||||
if e:
|
||||
h = sha256(cur)
|
||||
if e.get("applied") and h == e["applied"]:
|
||||
return MODIFIED
|
||||
if h == e["orig"]:
|
||||
return LOST if e.get("applied") else ORIGINAL
|
||||
return CHANGED
|
||||
if s.sha1 and sha1_16(cur) != s.sha1:
|
||||
return FOREIGN
|
||||
return ORIGINAL
|
||||
|
||||
def modified_keys(self):
|
||||
return [k for k, e in self.store.entries.items() if e.get("applied")]
|
||||
|
||||
def limit(self, s):
|
||||
"""Max size in bytes for a replacement (None = any size)."""
|
||||
if s.is_pack_media:
|
||||
return None
|
||||
e = self.store.get(s.key)
|
||||
if e:
|
||||
return e["orig_size"]
|
||||
if s.sha1 and s.size:
|
||||
return s.size
|
||||
return len(self.current_bytes(s))
|
||||
|
||||
def original_audio(self, s):
|
||||
return audio.decode_wem(self.original_bytes(s))
|
||||
|
||||
def current_audio(self, s):
|
||||
return audio.decode_wem(self.current_bytes(s))
|
||||
|
||||
# encoding ----------------------------------------------------------------------------------
|
||||
def prepare(self, s, x, src_rate, quality="auto", progress=None):
|
||||
"""Encode PCM for sound `s` (its rate/channels, fitted to its size limit) -> (wem, info)."""
|
||||
return encode_fit(x, src_rate, s.rate or src_rate, s.channels or x.shape[1], self.limit(s),
|
||||
quality, progress)
|
||||
|
||||
def prepare_file(self, s, path, quality="auto", progress=None):
|
||||
x, sr = audio.load(path)
|
||||
return self.prepare(s, x, sr, quality, progress)
|
||||
|
||||
# writing -----------------------------------------------------------------------------------
|
||||
def _backup(self, wad, pack_hashes):
|
||||
if not self.full_backup:
|
||||
return
|
||||
self.store.full_backup(wad, self.game.rel(wad))
|
||||
for h in pack_hashes:
|
||||
b = pack_bnk_path(wad, h)
|
||||
self.store.full_backup(b, self.game.rel(b))
|
||||
|
||||
def _write(self, archive, items, progress=None):
|
||||
"""items: [(sound, new_bytes, meta, is_revert)] all in one archive."""
|
||||
wad = self.game.wad(archive)
|
||||
self.game.check_writable(wad)
|
||||
packs = {parse_target(s.target)[0] for s, _, _, _ in items if s.is_pack_media}
|
||||
self._backup(wad, packs)
|
||||
w = Wad(wad)
|
||||
for h in packs:
|
||||
if self.store.pack_gaps(h) is None:
|
||||
self.store.remember_pack_gaps(h, Pack(open(pack_bnk_path(wad, h), "rb").read()).gaps(w.read(h)))
|
||||
repl, limits = {}, {}
|
||||
for s, data, meta, is_revert in items:
|
||||
if not is_revert:
|
||||
self.store.remember_original(s.key, s.archive, s.target, self.game.rel(wad),
|
||||
read_target(wad, s.target, w))
|
||||
repl[s.target] = data
|
||||
limits[s.target] = self.limit(s)
|
||||
self.store.save() # originals are safe on disk before the game file changes
|
||||
if progress:
|
||||
progress("writing %s" % os.path.basename(wad))
|
||||
inject(wad, repl, limits, pack_gaps={h: self.store.pack_gaps(h) for h in packs})
|
||||
for s, data, meta, is_revert in items:
|
||||
if is_revert:
|
||||
self.store.forget(s.key)
|
||||
else:
|
||||
self.store.record_applied(s.key, data, meta)
|
||||
self.store.save()
|
||||
|
||||
def apply(self, changes, force=False, progress=None):
|
||||
"""changes: [(sound, wem_bytes, meta dict)]. One atomic rewrite per WAD."""
|
||||
conflicts = []
|
||||
for s, _, _ in changes:
|
||||
if self.state(s) in (CHANGED, FOREIGN):
|
||||
conflicts.append(s)
|
||||
if conflicts and not force:
|
||||
raise ConflictError(conflicts)
|
||||
by_arch = defaultdict(list)
|
||||
for s, data, meta in changes:
|
||||
by_arch[s.archive].append((s, data, meta, False))
|
||||
for a, items in by_arch.items():
|
||||
self._write(a, items, progress)
|
||||
|
||||
def revert(self, sounds, progress=None):
|
||||
"""Put the stored originals back (sounds without a record are skipped). Returns reverted."""
|
||||
by_arch = defaultdict(list)
|
||||
done = []
|
||||
for s in sounds:
|
||||
orig = self.store.original(s.key)
|
||||
if orig is None:
|
||||
continue
|
||||
by_arch[s.archive].append((s, orig, None, True))
|
||||
done.append(s)
|
||||
for a, items in by_arch.items():
|
||||
self._write(a, items, progress)
|
||||
self.store.gc()
|
||||
return done
|
||||
|
||||
def revert_all(self, progress=None):
|
||||
sounds = [self.catalog.by_key[k] for k in list(self.store.entries) if k in self.catalog.by_key]
|
||||
missing = [k for k in self.store.entries if k not in self.catalog.by_key]
|
||||
if missing:
|
||||
# records of archives not loaded in this catalog (e.g. another language)
|
||||
other = Catalog(self.game, sorted({k.split(":")[0] for k in missing}))
|
||||
sounds += [other.by_key[k] for k in missing if k in other.by_key]
|
||||
return self.revert(sounds, progress)
|
||||
|
||||
def restore_from_full_backup(self):
|
||||
"""Last-resort fallback: copy back every full backup taken by this store."""
|
||||
out = []
|
||||
for rel in self.store.data.get("full", {}):
|
||||
src = self.store.full_copy(rel)
|
||||
dst = os.path.join(self.game.root, rel)
|
||||
self.game.check_writable(dst)
|
||||
tmp = dst + ".ackit-tmp"
|
||||
import shutil
|
||||
shutil.copy2(src, tmp)
|
||||
os.replace(tmp, dst)
|
||||
out.append(rel)
|
||||
self.store.data["entries"] = {}
|
||||
self.store.save()
|
||||
self.store.gc()
|
||||
return out
|
||||
221
ackit/wwvorbis.py
Normal file
221
ackit/wwvorbis.py
Normal file
@@ -0,0 +1,221 @@
|
||||
"""Wwise 2011 ("v56"-style) Vorbis <-> standard Vorbis conversion.
|
||||
|
||||
Wwise stores a stripped setup header (codebooks referenced by 10-bit IDs into the
|
||||
built-in library, narrowed fields) and "modified" audio packets (no packet-type bit,
|
||||
no prev/next window flags). See docs/wem_format.md.
|
||||
"""
|
||||
from .bits import BitReader, BitWriter, ilog
|
||||
from .codebooks import CodebookLibrary, wwise_book_to_vorbis, skip_vorbis_book, book_bits_key
|
||||
|
||||
W2V = "w2v" # Wwise -> Vorbis
|
||||
V2W = "v2w" # Vorbis -> Wwise
|
||||
|
||||
|
||||
class SetupInfo:
|
||||
def __init__(self):
|
||||
self.codebook_ids = []
|
||||
self.codebook_dims_entries = []
|
||||
self.mode_blockflags = []
|
||||
self.floor_count = 0
|
||||
self.residue_count = 0
|
||||
self.mapping_count = 0
|
||||
|
||||
@property
|
||||
def mode_bits(self):
|
||||
return ilog(len(self.mode_blockflags) - 1)
|
||||
|
||||
|
||||
def transcode_setup(src: bytes, direction: str, channels: int, lib: CodebookLibrary) -> (bytes, SetupInfo):
|
||||
"""Convert a setup packet between the Wwise stripped form and the standard Vorbis form."""
|
||||
info = SetupInfo()
|
||||
w = BitWriter()
|
||||
if direction == W2V:
|
||||
r = BitReader(src)
|
||||
w.put(8, 5)
|
||||
for c in b"vorbis":
|
||||
w.put(8, c)
|
||||
else:
|
||||
if src[:7] != b"\x05vorbis":
|
||||
raise ValueError("not a vorbis setup packet")
|
||||
r = BitReader(src, 7 * 8)
|
||||
|
||||
def f(ww_bits, vb_bits=None):
|
||||
"""Transcode one field; returns its value."""
|
||||
if vb_bits is None:
|
||||
vb_bits = ww_bits
|
||||
if direction == W2V:
|
||||
v = r.get(ww_bits); w.put(vb_bits, v)
|
||||
else:
|
||||
v = r.get(vb_bits); w.put(ww_bits, v)
|
||||
if v >> ww_bits:
|
||||
raise ValueError("value %d does not fit in %d Wwise bits" % (v, ww_bits))
|
||||
return v
|
||||
|
||||
def const(vb_bits, value, what):
|
||||
"""Field present only in Vorbis form, with a fixed value."""
|
||||
if direction == W2V:
|
||||
w.put(vb_bits, value)
|
||||
else:
|
||||
v = r.get(vb_bits)
|
||||
if v != value:
|
||||
raise ValueError("%s must be %d for Wwise, got %d" % (what, value, v))
|
||||
|
||||
# codebooks
|
||||
nbooks = f(8) + 1
|
||||
for _ in range(nbooks):
|
||||
if direction == W2V:
|
||||
cid = r.get(10)
|
||||
if cid >= len(lib.books):
|
||||
raise ValueError("codebook id %d out of range" % cid)
|
||||
wwise_book_to_vorbis(lib.books[cid], w)
|
||||
else:
|
||||
start = r.pos
|
||||
de = skip_vorbis_book(r)
|
||||
info.codebook_dims_entries.append(de)
|
||||
cid = lib.lookup(book_bits_key(src, start, r.pos))
|
||||
if cid is None:
|
||||
raise ValueError("codebook #%d not in game library (dims=%d entries=%d)" % (len(info.codebook_ids), de[0], de[1]))
|
||||
w.put(10, cid)
|
||||
info.codebook_ids.append(cid)
|
||||
|
||||
# time domain transforms (Vorbis only: count-1=0, one 16-bit zero)
|
||||
if direction == W2V:
|
||||
w.put(6, 0); w.put(16, 0)
|
||||
else:
|
||||
tc = r.get(6) + 1
|
||||
for _ in range(tc):
|
||||
if r.get(16) != 0:
|
||||
raise ValueError("nonzero time domain value")
|
||||
|
||||
# floors
|
||||
nfloors = f(6) + 1
|
||||
info.floor_count = nfloors
|
||||
for _ in range(nfloors):
|
||||
const(16, 1, "floor type")
|
||||
parts = f(5)
|
||||
classes = [f(4) for _ in range(parts)]
|
||||
maxc = max(classes) if classes else -1
|
||||
dims = []
|
||||
for _ in range(maxc + 1):
|
||||
dims.append(f(3) + 1)
|
||||
sub = f(2)
|
||||
if sub:
|
||||
f(8)
|
||||
for _ in range(1 << sub):
|
||||
f(8)
|
||||
f(2)
|
||||
rb = f(4)
|
||||
for c in classes:
|
||||
for _ in range(dims[c]):
|
||||
f(rb)
|
||||
|
||||
# residues
|
||||
nres = f(6) + 1
|
||||
info.residue_count = nres
|
||||
for _ in range(nres):
|
||||
rtype = f(2, 16)
|
||||
if rtype > 2:
|
||||
raise ValueError("bad residue type")
|
||||
f(24); f(24); f(24)
|
||||
ncls = f(6) + 1
|
||||
f(8)
|
||||
cascade = []
|
||||
for _ in range(ncls):
|
||||
lo = f(3)
|
||||
hi = f(5) if f(1) else 0
|
||||
cascade.append(hi * 8 + lo)
|
||||
for c in cascade:
|
||||
for k in range(8):
|
||||
if c & (1 << k):
|
||||
f(8)
|
||||
|
||||
# mappings
|
||||
nmap = f(6) + 1
|
||||
info.mapping_count = nmap
|
||||
for _ in range(nmap):
|
||||
const(16, 0, "mapping type")
|
||||
submaps = f(4) + 1 if f(1) else 1
|
||||
if f(1):
|
||||
steps = f(8) + 1
|
||||
b = ilog(channels - 1)
|
||||
for _ in range(steps):
|
||||
f(b); f(b)
|
||||
if f(2) != 0:
|
||||
raise ValueError("mapping reserved != 0")
|
||||
if submaps > 1:
|
||||
for _ in range(channels):
|
||||
f(4)
|
||||
for _ in range(submaps):
|
||||
f(8); f(8); f(8)
|
||||
|
||||
# modes
|
||||
nmodes = f(6) + 1
|
||||
for _ in range(nmodes):
|
||||
bf = f(1)
|
||||
const(16, 0, "window type")
|
||||
const(16, 0, "transform type")
|
||||
f(8)
|
||||
info.mode_blockflags.append(bf)
|
||||
|
||||
if direction == W2V:
|
||||
w.put(1, 1) # framing
|
||||
else:
|
||||
if r.get(1) != 1:
|
||||
raise ValueError("framing bit missing")
|
||||
return w.bytes(), info
|
||||
|
||||
|
||||
def build_id_header(channels: int, rate: int, bs0_exp: int, bs1_exp: int) -> bytes:
|
||||
import struct
|
||||
return (b"\x01vorbis" + struct.pack("<IBIiii", 0, channels, rate, 0, 0, 0)
|
||||
+ bytes([(bs1_exp << 4) | bs0_exp, 1]))
|
||||
|
||||
|
||||
def build_comment_header(vendor: bytes = b"ackit") -> bytes:
|
||||
import struct
|
||||
return b"\x03vorbis" + struct.pack("<I", len(vendor)) + vendor + struct.pack("<I", 0) + b"\x01"
|
||||
|
||||
|
||||
def ww_packet_to_vorbis(pkt: bytes, next_first: int, info: SetupInfo, prev_bf: int,
|
||||
modified: bool = True) -> (bytes, int):
|
||||
"""Rebuild a standard audio packet from a Wwise modified packet. Returns (packet, blockflag).
|
||||
When blocksizes are equal Wwise keeps standard packets (modified=False)."""
|
||||
mb = info.mode_bits
|
||||
if not modified:
|
||||
mode = (pkt[0] >> 1) & ((1 << mb) - 1)
|
||||
return pkt, info.mode_blockflags[mode]
|
||||
v = int.from_bytes(pkt, "little")
|
||||
nbits = len(pkt) * 8
|
||||
mode = v & ((1 << mb) - 1)
|
||||
bf = info.mode_blockflags[mode]
|
||||
out = 0 # packet type bit 0
|
||||
pos = 1
|
||||
out |= mode << pos; pos += mb
|
||||
if bf:
|
||||
nbf = 0 if next_first is None else info.mode_blockflags[next_first & ((1 << mb) - 1)]
|
||||
out |= prev_bf << pos; pos += 1
|
||||
out |= nbf << pos; pos += 1
|
||||
rest = v >> mb
|
||||
out |= rest << pos
|
||||
pos += nbits - mb
|
||||
return out.to_bytes((pos + 7) // 8, "little"), bf
|
||||
|
||||
|
||||
def vorbis_packet_to_ww(pkt: bytes, info: SetupInfo, modified: bool = True) -> (bytes, int):
|
||||
"""Strip packet-type bit and window flags. Returns (packet, blockflag)."""
|
||||
mb = info.mode_bits
|
||||
if not modified:
|
||||
if pkt[0] & 1:
|
||||
raise ValueError("not an audio packet")
|
||||
return pkt, info.mode_blockflags[(pkt[0] >> 1) & ((1 << mb) - 1)]
|
||||
v = int.from_bytes(pkt, "little")
|
||||
nbits = len(pkt) * 8
|
||||
if v & 1:
|
||||
raise ValueError("not an audio packet")
|
||||
mode = (v >> 1) & ((1 << mb) - 1)
|
||||
bf = info.mode_blockflags[mode]
|
||||
drop = 1 + mb + (2 if bf else 0)
|
||||
rest = v >> drop
|
||||
out = mode | (rest << mb)
|
||||
obits = nbits - (drop - mb)
|
||||
return out.to_bytes((obits + 7) // 8, "little"), bf
|
||||
1
app/__init__.py
Normal file
1
app/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Unheard City (PySide6 GUI on top of ackit)."""
|
||||
250
app/audioio.py
Normal file
250
app/audioio.py
Normal file
@@ -0,0 +1,250 @@
|
||||
"""Playback and recording with Qt Multimedia (bundled with PySide6; WASAPI on Windows,
|
||||
PulseAudio/PipeWire on Linux). Audio is exchanged as float32 numpy arrays (frames, channels).
|
||||
|
||||
Set ACKIT_NO_AUDIO=1 to disable devices (headless tests); `Recorder.start_simulated` feeds a recording from
|
||||
an array in real time, so the recording logic is testable without a microphone.
|
||||
"""
|
||||
import os
|
||||
|
||||
import numpy as np
|
||||
from PySide6.QtCore import QBuffer, QByteArray, QIODevice, QObject, QTimer, Signal
|
||||
from PySide6.QtMultimedia import QAudio, QAudioFormat, QAudioSink, QAudioSource, QMediaDevices
|
||||
|
||||
from ackit import audio
|
||||
|
||||
|
||||
def audio_enabled():
|
||||
return os.environ.get("ACKIT_NO_AUDIO") != "1"
|
||||
|
||||
|
||||
def output_devices():
|
||||
return list(QMediaDevices.audioOutputs()) if audio_enabled() else []
|
||||
|
||||
|
||||
def input_devices():
|
||||
return list(QMediaDevices.audioInputs()) if audio_enabled() else []
|
||||
|
||||
|
||||
def find_device(devices, description):
|
||||
return next((d for d in devices if d.description() == description), None)
|
||||
|
||||
|
||||
def _to_int16(x):
|
||||
return (np.clip(x, -1.0, 1.0) * 32767.0).astype("<i2").tobytes()
|
||||
|
||||
|
||||
class Player(QObject):
|
||||
"""Plays one buffer at a time. Signals: position(seconds), playing(bool), error(str)."""
|
||||
position = Signal(float)
|
||||
playing = Signal(bool)
|
||||
error = Signal(str)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.device = None
|
||||
self.sink = None
|
||||
self.buffer = None
|
||||
self.data = QByteArray()
|
||||
self.fmt = None
|
||||
self.base = 0.0
|
||||
self.duration = 0.0
|
||||
self.loop = False
|
||||
self.volume = 1.0
|
||||
self.label = None
|
||||
self.timer = QTimer(self)
|
||||
self.timer.setInterval(40)
|
||||
self.timer.timeout.connect(self._tick)
|
||||
|
||||
def set_device(self, device):
|
||||
self.stop()
|
||||
self.device = device
|
||||
|
||||
def _device(self):
|
||||
if self.device is not None and not self.device.isNull():
|
||||
return self.device
|
||||
d = QMediaDevices.defaultAudioOutput() if audio_enabled() else None
|
||||
return d if d is not None and not d.isNull() else None
|
||||
|
||||
def play(self, x, rate, start=0.0, loop=False, label=None):
|
||||
"""x: float32 (n, ch). Converted to the device's format (rate, channels, 16-bit)."""
|
||||
self.stop()
|
||||
dev = self._device()
|
||||
if dev is None:
|
||||
self.error.emit("No audio output device available.")
|
||||
return False
|
||||
pref = dev.preferredFormat()
|
||||
fmt = QAudioFormat()
|
||||
fmt.setSampleRate(pref.sampleRate() or 48000)
|
||||
fmt.setChannelCount(min(2, pref.channelCount() or 2))
|
||||
fmt.setSampleFormat(QAudioFormat.SampleFormat.Int16)
|
||||
if not dev.isFormatSupported(fmt):
|
||||
fmt.setChannelCount(2)
|
||||
fmt.setSampleRate(48000)
|
||||
y = audio.to_channels(audio.resample(x, rate, fmt.sampleRate()), fmt.channelCount())
|
||||
self.fmt, self.loop, self.label = fmt, loop, label
|
||||
self.data = QByteArray(_to_int16(y))
|
||||
self.duration = len(y) / float(fmt.sampleRate())
|
||||
self.sink = QAudioSink(dev, fmt, self)
|
||||
self.sink.setVolume(self.volume)
|
||||
self.sink.stateChanged.connect(self._state)
|
||||
self._start_at(start)
|
||||
return True
|
||||
|
||||
def _start_at(self, seconds):
|
||||
seconds = max(0.0, min(seconds, self.duration))
|
||||
if self.buffer is not None:
|
||||
self.buffer.close()
|
||||
self.buffer = QBuffer(self)
|
||||
self.buffer.setData(self.data)
|
||||
self.buffer.open(QIODevice.OpenModeFlag.ReadOnly)
|
||||
frame = self.fmt.bytesPerFrame()
|
||||
self.buffer.seek(int(seconds * self.fmt.sampleRate()) * frame)
|
||||
self.base = seconds
|
||||
self.sink.start(self.buffer)
|
||||
self.timer.start()
|
||||
self.playing.emit(True)
|
||||
|
||||
def seek(self, seconds):
|
||||
if self.sink is not None:
|
||||
self.sink.stop()
|
||||
self._start_at(seconds)
|
||||
|
||||
def set_volume(self, v):
|
||||
self.volume = float(v)
|
||||
if self.sink is not None:
|
||||
self.sink.setVolume(self.volume)
|
||||
|
||||
def is_playing(self):
|
||||
return self.sink is not None and self.sink.state() == QAudio.State.ActiveState
|
||||
|
||||
def current_time(self):
|
||||
if self.sink is None:
|
||||
return 0.0
|
||||
return self.base + self.sink.processedUSecs() / 1e6
|
||||
|
||||
def _tick(self):
|
||||
self.position.emit(min(self.current_time(), self.duration))
|
||||
|
||||
def _state(self, st):
|
||||
if st == QAudio.State.IdleState: # reached the end of the buffer
|
||||
if self.loop:
|
||||
self.sink.stop()
|
||||
self._start_at(0.0)
|
||||
else:
|
||||
self.stop()
|
||||
|
||||
def stop(self):
|
||||
self.timer.stop()
|
||||
if self.sink is not None:
|
||||
try:
|
||||
self.sink.stateChanged.disconnect(self._state)
|
||||
except (RuntimeError, TypeError):
|
||||
pass
|
||||
self.sink.stop()
|
||||
self.sink.deleteLater()
|
||||
self.sink = None
|
||||
self.playing.emit(False)
|
||||
if self.buffer is not None:
|
||||
self.buffer.close()
|
||||
self.buffer = None
|
||||
|
||||
|
||||
class Recorder(QObject):
|
||||
"""Records from an input device. Signals: level(peak 0..1), elapsed(seconds), error(str)."""
|
||||
level = Signal(float)
|
||||
elapsed = Signal(float)
|
||||
error = Signal(str)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.device = None
|
||||
self.source = None
|
||||
self.io = None
|
||||
self.chunks = []
|
||||
self.rate = 48000
|
||||
self.channels = 1
|
||||
self.fmt_float = False
|
||||
self._sim = None
|
||||
|
||||
def set_device(self, device):
|
||||
self.device = device
|
||||
|
||||
def is_recording(self):
|
||||
return self.source is not None or self._sim is not None
|
||||
|
||||
def start(self):
|
||||
self.chunks = []
|
||||
dev = self.device if (self.device is not None and not self.device.isNull()) else (
|
||||
QMediaDevices.defaultAudioInput() if audio_enabled() else None)
|
||||
if dev is None or dev.isNull():
|
||||
self.error.emit("No audio input device available.")
|
||||
return False
|
||||
fmt = QAudioFormat()
|
||||
fmt.setSampleRate(48000)
|
||||
fmt.setChannelCount(1)
|
||||
fmt.setSampleFormat(QAudioFormat.SampleFormat.Int16)
|
||||
if not dev.isFormatSupported(fmt):
|
||||
fmt = dev.preferredFormat()
|
||||
self.rate, self.channels = fmt.sampleRate(), fmt.channelCount()
|
||||
self.fmt_float = fmt.sampleFormat() == QAudioFormat.SampleFormat.Float
|
||||
self.source = QAudioSource(dev, fmt, self)
|
||||
self.io = self.source.start()
|
||||
if self.io is None:
|
||||
self.error.emit("Could not open the input device.")
|
||||
self.source = None
|
||||
return False
|
||||
self.io.readyRead.connect(self._read)
|
||||
return True
|
||||
|
||||
def _read(self):
|
||||
if self.io is None:
|
||||
return
|
||||
self._ingest(bytes(self.io.readAll()))
|
||||
|
||||
def _ingest(self, raw):
|
||||
if not raw:
|
||||
return
|
||||
if self.fmt_float:
|
||||
x = np.frombuffer(raw, dtype="<f4")
|
||||
else:
|
||||
x = np.frombuffer(raw[: len(raw) // 2 * 2], dtype="<i2").astype(np.float32) / 32768.0
|
||||
x = x[: len(x) // self.channels * self.channels].reshape(-1, self.channels)
|
||||
self.chunks.append(x)
|
||||
self.level.emit(float(np.max(np.abs(x))) if len(x) else 0.0)
|
||||
self.elapsed.emit(sum(len(c) for c in self.chunks) / float(self.rate))
|
||||
|
||||
# simulated input (tests / no microphone) ----------------------------------------------------
|
||||
def start_simulated(self, x, rate, realtime=True):
|
||||
"""Feed `x` as if it came from a microphone (20 ms blocks)."""
|
||||
self.chunks = []
|
||||
self.rate, self.channels, self.fmt_float = rate, x.shape[1], True
|
||||
blocks = [x[i:i + rate // 50] for i in range(0, len(x), rate // 50)]
|
||||
if not realtime:
|
||||
for b in blocks:
|
||||
self._ingest(np.ascontiguousarray(b, dtype="<f4").tobytes())
|
||||
return True
|
||||
self._sim = (QTimer(self), iter(blocks))
|
||||
def step():
|
||||
b = next(self._sim[1], None) if self._sim else None
|
||||
if b is None:
|
||||
if self._sim:
|
||||
self._sim[0].stop()
|
||||
return
|
||||
self._ingest(np.ascontiguousarray(b, dtype="<f4").tobytes())
|
||||
self._sim[0].timeout.connect(step)
|
||||
self._sim[0].start(20)
|
||||
return True
|
||||
|
||||
def stop(self):
|
||||
"""-> (float32 array (n, ch), rate) of everything recorded."""
|
||||
if self.source is not None:
|
||||
self._read()
|
||||
self.source.stop()
|
||||
self.source.deleteLater()
|
||||
self.source = None
|
||||
self.io = None
|
||||
if self._sim is not None:
|
||||
self._sim[0].stop()
|
||||
self._sim = None
|
||||
x = np.concatenate(self.chunks) if self.chunks else np.zeros((0, self.channels), np.float32)
|
||||
return x, self.rate
|
||||
163
app/dialogs.py
Normal file
163
app/dialogs.py
Normal file
@@ -0,0 +1,163 @@
|
||||
"""Settings, mod export and mod import dialogs."""
|
||||
import os
|
||||
|
||||
from PySide6.QtCore import Qt
|
||||
from PySide6.QtWidgets import (QCheckBox, QComboBox, QDialog, QDialogButtonBox, QFileDialog, QFormLayout, QHBoxLayout,
|
||||
QLabel, QLineEdit, QListWidget, QListWidgetItem, QPlainTextEdit, QPushButton, QSlider,
|
||||
QTreeWidget, QTreeWidgetItem, QVBoxLayout)
|
||||
|
||||
from ackit.game import find_installs, is_game_dir
|
||||
from .audioio import output_devices
|
||||
|
||||
|
||||
def pick_game_dir(parent, start=""):
|
||||
"""Folder chooser that explains what to pick (also Proton/Linux paths)."""
|
||||
d = QFileDialog.getExistingDirectory(
|
||||
parent, "Select the Batman: Arkham City folder (contains BmGame and Binaries)", start or os.path.expanduser("~"))
|
||||
return d or None
|
||||
|
||||
|
||||
class SettingsDialog(QDialog):
|
||||
def __init__(self, settings, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setWindowTitle("Settings")
|
||||
self.settings = dict(settings)
|
||||
f = QFormLayout(self)
|
||||
self.game = QLineEdit(settings.get("game_dir") or "")
|
||||
b = QPushButton("Browse…")
|
||||
b.clicked.connect(self._browse_game)
|
||||
row = QHBoxLayout()
|
||||
row.addWidget(self.game, 1)
|
||||
row.addWidget(b)
|
||||
found = find_installs()
|
||||
hint = QLabel("Found via Steam: " + ("; ".join(found) if found else "none (Linux/Proton: "
|
||||
"~/.steam/steam/steamapps/common/Batman Arkham City GOTY)"))
|
||||
hint.setWordWrap(True)
|
||||
f.addRow("Game folder", row)
|
||||
f.addRow("", hint)
|
||||
self.store = QLineEdit(settings.get("store_root") or "")
|
||||
self.store.setPlaceholderText("default: in your user data folder")
|
||||
b2 = QPushButton("Browse…")
|
||||
b2.clicked.connect(lambda: self._browse(self.store))
|
||||
row2 = QHBoxLayout()
|
||||
row2.addWidget(self.store, 1)
|
||||
row2.addWidget(b2)
|
||||
f.addRow("Originals store", row2)
|
||||
self.full = QCheckBox("Keep one full copy of each audio archive before its first change (safest)")
|
||||
self.full.setChecked(settings.get("full_backup", True))
|
||||
f.addRow("", self.full)
|
||||
self.out = QComboBox()
|
||||
self.out.addItem("Default output")
|
||||
for d in output_devices():
|
||||
self.out.addItem(d.description())
|
||||
if settings.get("output_device"):
|
||||
i = self.out.findText(settings["output_device"])
|
||||
if i >= 0:
|
||||
self.out.setCurrentIndex(i)
|
||||
f.addRow("Playback device", self.out)
|
||||
# appearance
|
||||
self.bg_on = QCheckBox("Show the Arkham City wallpaper behind the filters")
|
||||
self.bg_on.setChecked(settings.get("background_enabled", True))
|
||||
f.addRow("Background", self.bg_on)
|
||||
self.opacity = QSlider(Qt.Orientation.Horizontal)
|
||||
self.opacity.setRange(0, 100)
|
||||
self.opacity.setValue(int(round(float(settings.get("background_opacity", 0.1)) * 100)))
|
||||
f.addRow("Background strength", self.opacity)
|
||||
self.bg_on.toggled.connect(self.opacity.setEnabled)
|
||||
self.opacity.setEnabled(self.bg_on.isChecked())
|
||||
bb = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
|
||||
bb.accepted.connect(self.accept)
|
||||
bb.rejected.connect(self.reject)
|
||||
f.addRow(bb)
|
||||
self.resize(640, 0)
|
||||
|
||||
def _browse_game(self):
|
||||
d = pick_game_dir(self, self.game.text())
|
||||
if d:
|
||||
self.game.setText(d)
|
||||
|
||||
def _browse(self, edit):
|
||||
d = QFileDialog.getExistingDirectory(self, "Choose folder", edit.text())
|
||||
if d:
|
||||
edit.setText(d)
|
||||
|
||||
def values(self):
|
||||
s = dict(self.settings)
|
||||
gd = self.game.text().strip()
|
||||
s["game_dir"] = gd if is_game_dir(gd) else s.get("game_dir")
|
||||
s["store_root"] = self.store.text().strip() or None
|
||||
s["full_backup"] = self.full.isChecked()
|
||||
s["output_device"] = self.out.currentText() if self.out.currentIndex() > 0 else None
|
||||
s["background_enabled"] = self.bg_on.isChecked()
|
||||
s["background_opacity"] = self.opacity.value() / 100.0
|
||||
return s
|
||||
|
||||
|
||||
class ExportModDialog(QDialog):
|
||||
"""Pick which modified sounds go into a .acmod package."""
|
||||
|
||||
def __init__(self, items, parent=None):
|
||||
"""items: [(key, name)]"""
|
||||
super().__init__(parent)
|
||||
self.setWindowTitle("Export mod package")
|
||||
lay = QVBoxLayout(self)
|
||||
f = QFormLayout()
|
||||
self.title = QLineEdit()
|
||||
self.author = QLineEdit()
|
||||
self.desc = QPlainTextEdit()
|
||||
self.desc.setMaximumHeight(70)
|
||||
f.addRow("Title", self.title)
|
||||
f.addRow("Author", self.author)
|
||||
f.addRow("Description", self.desc)
|
||||
lay.addLayout(f)
|
||||
lay.addWidget(QLabel("Sounds to include:"))
|
||||
self.list = QListWidget()
|
||||
for key, name in items:
|
||||
it = QListWidgetItem(name)
|
||||
it.setData(Qt.ItemDataRole.UserRole, key)
|
||||
it.setFlags(it.flags() | Qt.ItemFlag.ItemIsUserCheckable)
|
||||
it.setCheckState(Qt.CheckState.Checked)
|
||||
self.list.addItem(it)
|
||||
lay.addWidget(self.list)
|
||||
bb = QDialogButtonBox(QDialogButtonBox.StandardButton.Save | QDialogButtonBox.StandardButton.Cancel)
|
||||
bb.accepted.connect(self.accept)
|
||||
bb.rejected.connect(self.reject)
|
||||
lay.addWidget(bb)
|
||||
self.resize(520, 480)
|
||||
|
||||
def selected_keys(self):
|
||||
return [self.list.item(i).data(Qt.ItemDataRole.UserRole) for i in range(self.list.count())
|
||||
if self.list.item(i).checkState() == Qt.CheckState.Checked]
|
||||
|
||||
|
||||
class ImportModDialog(QDialog):
|
||||
"""Shows how each item of a package matches this game before installing."""
|
||||
|
||||
def __init__(self, manifest, items, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setWindowTitle("Import mod package")
|
||||
lay = QVBoxLayout(self)
|
||||
head = "<b>%s</b>" % (manifest.get("name") or "Mod package")
|
||||
if manifest.get("author"):
|
||||
head += " by %s" % manifest["author"]
|
||||
lab = QLabel(head + ("<br>" + manifest["description"] if manifest.get("description") else ""))
|
||||
lab.setWordWrap(True)
|
||||
lay.addWidget(lab)
|
||||
self.tree = QTreeWidget()
|
||||
self.tree.setHeaderLabels(["Sound", "Match", "Note"])
|
||||
n_ok = 0
|
||||
for im in items:
|
||||
note = {"exact": "same sound", "by name": "matched by name (other language) - will be re-encoded",
|
||||
"missing": im.note}[im.how]
|
||||
QTreeWidgetItem(self.tree, [im.item.get("name") or im.item["target"], im.how, note])
|
||||
n_ok += im.how != "missing"
|
||||
self.tree.resizeColumnToContents(0)
|
||||
lay.addWidget(self.tree)
|
||||
lay.addWidget(QLabel("%d of %d sounds will be installed." % (n_ok, len(items))))
|
||||
bb = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
|
||||
bb.button(QDialogButtonBox.StandardButton.Ok).setText("Install")
|
||||
bb.button(QDialogButtonBox.StandardButton.Ok).setEnabled(n_ok > 0)
|
||||
bb.accepted.connect(self.accept)
|
||||
bb.rejected.connect(self.reject)
|
||||
lay.addWidget(bb)
|
||||
self.resize(720, 420)
|
||||
526
app/editor.py
Normal file
526
app/editor.py
Normal file
@@ -0,0 +1,526 @@
|
||||
"""Single-sound editor: listen, substitute (file or recording), process, apply / revert."""
|
||||
import os
|
||||
|
||||
import numpy as np
|
||||
from PySide6.QtCore import Qt, QTimer, Signal
|
||||
from PySide6.QtGui import QFont
|
||||
from PySide6.QtWidgets import (QCheckBox, QComboBox, QDoubleSpinBox, QFileDialog, QGridLayout, QGroupBox,
|
||||
QHBoxLayout, QLabel, QMessageBox, QProgressBar, QPushButton, QSlider,
|
||||
QVBoxLayout, QWidget)
|
||||
|
||||
from ackit import audio, workspace as W
|
||||
from . import icons, theme
|
||||
from .audioio import Recorder, input_devices, find_device
|
||||
from .spinner import Spinner
|
||||
from .waveform import Waveform
|
||||
|
||||
AUDIO_FILTER = "Audio files (*.wav *.flac *.ogg *.oga *.opus *.mp3 *.aif *.aiff *.m4a *.aac *.wma);;All files (*)"
|
||||
STATE_TEXT = {W.ORIGINAL: "Original", W.MODIFIED: "Modified (your replacement is installed)",
|
||||
W.LOST: "Your change was undone outside the tool (e.g. Steam 'verify files')",
|
||||
W.CHANGED: "Changed outside this tool (Steam update or another mod tool)",
|
||||
W.FOREIGN: "Differs from the original game file (modified by another tool?)"}
|
||||
|
||||
|
||||
def fmt_s(t):
|
||||
return "%d:%05.2f" % (int(t // 60), t % 60)
|
||||
|
||||
|
||||
class EditorPanel(QWidget):
|
||||
applied = Signal(str) # sound key
|
||||
reverted = Signal(str)
|
||||
message = Signal(str)
|
||||
|
||||
def __init__(self, ctx, parent=None):
|
||||
super().__init__(parent)
|
||||
self.ctx = ctx # main window: .ws, .runner, .player, .settings
|
||||
self.sound = None
|
||||
self.orig = None # (x, rate)
|
||||
self.installed = None
|
||||
self.src = None # replacement source (x, rate) before processing
|
||||
self.src_name = ""
|
||||
self.playing_what = None
|
||||
self.recorder = Recorder(self)
|
||||
self._build()
|
||||
self.set_sound(None)
|
||||
|
||||
# --- UI --------------------------------------------------------------------------------------
|
||||
def _btn(self, text, icon=None, slot=None, tip=None, color=None):
|
||||
b = QPushButton(text)
|
||||
if icon is not None:
|
||||
b.setIcon(icons.icon(icon, color or icons.TEXT))
|
||||
if slot:
|
||||
b.clicked.connect(slot)
|
||||
if tip:
|
||||
b.setToolTip(tip)
|
||||
return b
|
||||
|
||||
def _wave(self, kind, selectable=False):
|
||||
w = Waveform(selectable=selectable)
|
||||
w.accent, w.bg = theme.WAVE[kind], theme.WAVE_BG
|
||||
return w
|
||||
|
||||
def _build(self):
|
||||
self.setObjectName("EditorPanel")
|
||||
lay = QVBoxLayout(self)
|
||||
lay.setContentsMargins(10, 8, 10, 8)
|
||||
self.title = QLabel()
|
||||
self.title.setObjectName("EditorTitle")
|
||||
self.title.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse)
|
||||
f = self.title.font()
|
||||
f.setPointSizeF(f.pointSizeF() * 1.35)
|
||||
f.setBold(True)
|
||||
f.setLetterSpacing(QFont.SpacingType.PercentageSpacing, 104)
|
||||
self.title.setFont(f)
|
||||
lay.addWidget(self.title)
|
||||
self.meta = QLabel()
|
||||
self.meta.setWordWrap(True)
|
||||
self.meta.setObjectName("Muted")
|
||||
self.state = QLabel()
|
||||
self.state.setWordWrap(True)
|
||||
lay.addWidget(self.meta)
|
||||
lay.addWidget(self.state)
|
||||
|
||||
# listen ----------------------------------------------------------------------------------
|
||||
g = QGroupBox("LISTEN")
|
||||
gl = QGridLayout(g)
|
||||
self.wf_orig = self._wave("orig")
|
||||
self.wf_inst = self._wave("inst")
|
||||
self.b_play_orig = self._btn("Original", "play", lambda: self.play("orig"), "Play the original (Space)", icons.STEEL)
|
||||
self.b_play_inst = self._btn("Installed", "play", lambda: self.play("inst"), "Play what the game has now", "#8f4a4e")
|
||||
self.b_stop = self._btn("Stop", "stop", self.stop, "Stop (Esc)")
|
||||
self.b_ab = self._btn("A/B", "ab", self.ab_switch, "Switch between original and replacement at the same position",
|
||||
icons.ACCENT)
|
||||
self.cb_loop = self._btn("Loop", "loop", None, "Repeat playback")
|
||||
self.cb_loop.setCheckable(True)
|
||||
self.vol_icon = QLabel()
|
||||
self.vol_icon.setPixmap(icons.icon("play", icons.MUTED).pixmap(14, 14))
|
||||
self.sl_vol = QSlider(Qt.Orientation.Horizontal)
|
||||
self.sl_vol.setRange(0, 100)
|
||||
self.sl_vol.setValue(80)
|
||||
self.sl_vol.setMaximumWidth(120)
|
||||
self.sl_vol.setToolTip("Volume")
|
||||
self.sl_vol.valueChanged.connect(lambda v: self.ctx.player.set_volume(v / 100.0))
|
||||
self.lbl_time = QLabel("0:00.00")
|
||||
self.lbl_time.setObjectName("Muted")
|
||||
gl.addWidget(QLabel("Original"), 0, 0)
|
||||
gl.addWidget(self.wf_orig, 0, 1, 1, 6)
|
||||
self.lbl_inst = QLabel("Installed")
|
||||
gl.addWidget(self.lbl_inst, 1, 0)
|
||||
gl.addWidget(self.wf_inst, 1, 1, 1, 6)
|
||||
row = QHBoxLayout()
|
||||
for w in (self.b_play_orig, self.b_play_inst, self.b_stop, self.b_ab, self.cb_loop, QLabel(" Volume"),
|
||||
self.sl_vol, self.lbl_time):
|
||||
row.addWidget(w)
|
||||
row.addStretch(1)
|
||||
gl.addLayout(row, 2, 0, 1, 7)
|
||||
gl.setColumnStretch(1, 1)
|
||||
lay.addWidget(g)
|
||||
self.wf_orig.seekRequested.connect(lambda t: self.play("orig", t))
|
||||
self.wf_inst.seekRequested.connect(lambda t: self.play("inst", t))
|
||||
|
||||
# replace ---------------------------------------------------------------------------------
|
||||
g = QGroupBox("REPLACE")
|
||||
rl = QVBoxLayout(g)
|
||||
src = QHBoxLayout()
|
||||
self.b_load = self._btn("Load file…", "open", self.load_file_dialog,
|
||||
"WAV, FLAC, OGG, OPUS, MP3, AIFF (M4A/WMA need ffmpeg installed) (Ctrl+O)")
|
||||
self.cmb_in = QComboBox()
|
||||
self.cmb_in.setMinimumWidth(180)
|
||||
self.cmb_in.setToolTip("Recording device")
|
||||
self.b_rec = self._btn("Record", "record", self.toggle_record, "Record from the selected input")
|
||||
self.meter = QProgressBar()
|
||||
self.meter.setRange(0, 100)
|
||||
self.meter.setTextVisible(False)
|
||||
self.meter.setMaximumWidth(120)
|
||||
self.meter.setToolTip("Input level")
|
||||
self.lbl_rec = QLabel("")
|
||||
self.lbl_rec.setObjectName("Muted")
|
||||
for w in (self.b_load, QLabel(" or"), self.cmb_in, self.b_rec, self.meter, self.lbl_rec):
|
||||
src.addWidget(w)
|
||||
src.addStretch(1)
|
||||
rl.addLayout(src)
|
||||
self.wf_new = self._wave("new", selectable=True)
|
||||
self.wf_new.setToolTip("Click: play from here. Right-drag (or Shift+drag): select the part to keep.")
|
||||
self.wf_new.seekRequested.connect(lambda t: self.play("new", t))
|
||||
self.wf_new.selectionChanged.connect(self._selection)
|
||||
rl.addWidget(self.wf_new)
|
||||
proc = QHBoxLayout()
|
||||
self.sp_a = QDoubleSpinBox()
|
||||
self.sp_b = QDoubleSpinBox()
|
||||
for s_ in (self.sp_a, self.sp_b):
|
||||
s_.setDecimals(2)
|
||||
s_.setSuffix(" s")
|
||||
s_.setRange(0, 36000)
|
||||
s_.valueChanged.connect(self._changed)
|
||||
self.sp_gain = QDoubleSpinBox()
|
||||
self.sp_gain.setRange(-40, 40)
|
||||
self.sp_gain.setSuffix(" dB")
|
||||
self.sp_gain.valueChanged.connect(self._changed)
|
||||
self.cb_norm = QCheckBox("Normalize")
|
||||
self.cb_match = QCheckBox("Match original loudness")
|
||||
self.cb_norm.toggled.connect(self._changed)
|
||||
self.cb_match.toggled.connect(self._changed)
|
||||
self.b_silence = self._btn("Trim silence", None, self.trim_silence, "Cut silence at the start and end")
|
||||
self.b_play_new = self._btn("Replacement", "play", lambda: self.play("new"), "Play the processed replacement",
|
||||
icons.ACCENT)
|
||||
for w in (QLabel("Keep from"), self.sp_a, QLabel("to"), self.sp_b, self.b_silence, QLabel(" Gain"),
|
||||
self.sp_gain, self.cb_norm, self.cb_match, self.b_play_new):
|
||||
proc.addWidget(w)
|
||||
proc.addStretch(1)
|
||||
rl.addLayout(proc)
|
||||
info = QHBoxLayout()
|
||||
self.lbl_new = QLabel()
|
||||
self.lbl_new.setWordWrap(True)
|
||||
self.lbl_new.setObjectName("Muted")
|
||||
self.warn_icon = QLabel()
|
||||
self.warn_icon.setPixmap(icons.icon("warning", icons.ACCENT).pixmap(18, 18))
|
||||
self.warn_icon.hide()
|
||||
self.lbl_warn = QLabel()
|
||||
self.lbl_warn.setWordWrap(True)
|
||||
self.lbl_warn.setStyleSheet("color: %s;" % icons.ACCENT)
|
||||
self.lbl_new.setWordWrap(False)
|
||||
info.addWidget(self.lbl_new, 0, Qt.AlignmentFlag.AlignTop)
|
||||
info.addSpacing(12)
|
||||
info.addWidget(self.warn_icon, 0, Qt.AlignmentFlag.AlignTop)
|
||||
info.addWidget(self.lbl_warn, 1)
|
||||
rl.addLayout(info)
|
||||
lay.addWidget(g)
|
||||
|
||||
# apply -----------------------------------------------------------------------------------
|
||||
act = QHBoxLayout()
|
||||
self.b_apply = self._btn("Apply to game", "apply", self.apply,
|
||||
"Encode the replacement and install it in the game files (Ctrl+S)", "#ffffff")
|
||||
self.b_apply.setObjectName("Primary")
|
||||
self.b_revert = self._btn("Revert to original", "revert", self.revert, "Put the game's original back")
|
||||
self.b_export = self._btn("Save as WAV…", "save", self.export_wav,
|
||||
"Save the original (or the replacement) as a WAV file")
|
||||
self.busy = Spinner(16)
|
||||
self.lbl_busy = QLabel()
|
||||
for w in (self.b_apply, self.b_revert, self.b_export, self.busy, self.lbl_busy):
|
||||
act.addWidget(w)
|
||||
act.addStretch(1)
|
||||
lay.addLayout(act)
|
||||
lay.addStretch(1)
|
||||
|
||||
self.recorder.level.connect(lambda v: self.meter.setValue(int(min(1.0, v) * 100)))
|
||||
self.recorder.elapsed.connect(lambda t: self.lbl_rec.setText("recording %s" % fmt_s(t)))
|
||||
self.recorder.error.connect(lambda m: self.inform("Recording", m))
|
||||
self.ctx.player.position.connect(self._pos)
|
||||
self.ctx.player.playing.connect(lambda on: None if on else self._clear_playheads())
|
||||
self._reproc = QTimer(self)
|
||||
self._reproc.setSingleShot(True)
|
||||
self._reproc.setInterval(150)
|
||||
self._reproc.timeout.connect(self._update_new)
|
||||
|
||||
def refresh_devices(self):
|
||||
cur = self.ctx.settings.get("input_device")
|
||||
self.cmb_in.clear()
|
||||
self.cmb_in.addItem("Default input")
|
||||
for d in input_devices():
|
||||
self.cmb_in.addItem(d.description())
|
||||
if cur:
|
||||
i = self.cmb_in.findText(cur)
|
||||
if i >= 0:
|
||||
self.cmb_in.setCurrentIndex(i)
|
||||
|
||||
# --- dialogs (overridable in tests) -----------------------------------------------------------
|
||||
def confirm(self, title, text):
|
||||
return QMessageBox.question(self, title, text) == QMessageBox.StandardButton.Yes
|
||||
|
||||
def inform(self, title, text):
|
||||
QMessageBox.information(self, title, text)
|
||||
|
||||
def ask_open(self):
|
||||
p, _ = QFileDialog.getOpenFileName(self, "Choose replacement audio", self.ctx.settings.get("last_dir") or "",
|
||||
AUDIO_FILTER)
|
||||
return p
|
||||
|
||||
def ask_save(self, default):
|
||||
p, _ = QFileDialog.getSaveFileName(self, "Save as WAV", default, "WAV (*.wav)")
|
||||
return p
|
||||
|
||||
# --- loading ---------------------------------------------------------------------------------
|
||||
def set_sound(self, s):
|
||||
self.stop()
|
||||
if self.recorder.is_recording():
|
||||
self.recorder.stop()
|
||||
self.sound, self.orig, self.installed, self.src = s, None, None, None
|
||||
enabled = s is not None
|
||||
for w in self.findChildren(QPushButton) + [self.sp_a, self.sp_b, self.sp_gain, self.cb_norm, self.cb_match]:
|
||||
w.setEnabled(enabled)
|
||||
self.wf_orig.set_audio(None, 1, "select a sound" if s is None else "loading…", busy=s is not None)
|
||||
self.wf_inst.set_audio(None, 1, "")
|
||||
self.wf_new.set_audio(None, 1, "load a file or record a replacement")
|
||||
self.lbl_new.setText("")
|
||||
if s is None:
|
||||
self.title.setText("No sound selected")
|
||||
self.meta.setText("Search and select a sound in the list.")
|
||||
self.state.setText("")
|
||||
return
|
||||
self.title.setText(s.name)
|
||||
who = s.character or (("mentions " + s.characters_inferred) if s.characters_inferred else "—")
|
||||
lim = self.ctx.ws.limit(s)
|
||||
self.meta.setText("%s / %s • %s • %.2f s, %d Hz, %s • max size: %s • %s"
|
||||
% (s.group, s.category, who, s.seconds, s.rate,
|
||||
{1: "mono", 2: "stereo"}.get(s.channels, "%d ch" % s.channels),
|
||||
"any" if lim is None else "%.1f KB" % (lim / 1024.0), s.key))
|
||||
key = s.key
|
||||
ws = self.ctx.ws
|
||||
|
||||
def work(job):
|
||||
cur = ws.current_bytes(s)
|
||||
st = ws.state(s, cur)
|
||||
o = audio.decode_wem(ws.original_bytes(s))
|
||||
inst = audio.decode_wem(cur) if st != W.ORIGINAL else None
|
||||
return key, st, o, inst
|
||||
|
||||
self.ctx.runner.start(work, self._loaded, lambda e, tb: self._failed("Loading the sound failed", e))
|
||||
|
||||
def _loaded(self, r):
|
||||
key, st, o, inst = r
|
||||
if not self.sound or self.sound.key != key:
|
||||
return
|
||||
self.orig, self.installed = o, inst
|
||||
self.wf_orig.set_audio(*o)
|
||||
self.wf_inst.set_audio(*(inst or (None, 1)), placeholder="same as original")
|
||||
self.b_play_inst.setEnabled(inst is not None)
|
||||
self.b_revert.setEnabled(st != W.ORIGINAL or bool(self.ctx.ws.store.get(key)))
|
||||
self.state.setText("State: " + STATE_TEXT.get(st, st))
|
||||
self._changed()
|
||||
|
||||
# --- replacement source ----------------------------------------------------------------------
|
||||
def load_file_dialog(self):
|
||||
p = self.ask_open()
|
||||
if p:
|
||||
self.ctx.settings["last_dir"] = os.path.dirname(p)
|
||||
self.load_file(p)
|
||||
|
||||
def load_file(self, path):
|
||||
"""Decode in the background (spinner in the replacement waveform); False if it already failed."""
|
||||
name = os.path.basename(path)
|
||||
self.wf_new.set_audio(None, 1, "loading %s…" % name, busy=True)
|
||||
result = []
|
||||
|
||||
def done(r):
|
||||
result.append(True)
|
||||
self.set_source(r[0], r[1], name)
|
||||
|
||||
def failed(e, tb):
|
||||
result.append(False)
|
||||
self.wf_new.set_audio(None, 1, "load a file or record a replacement")
|
||||
if isinstance(e, audio.AudioError):
|
||||
self.inform("Unsupported audio", str(e))
|
||||
else:
|
||||
self._failed("Loading the file failed", e)
|
||||
|
||||
self.ctx.runner.start(lambda job: audio.load(path), done, failed)
|
||||
return result[0] if result else True
|
||||
|
||||
def set_source(self, x, sr, name):
|
||||
self.src, self.src_name = (np.asarray(x, np.float32), sr), name
|
||||
dur = audio.duration(x, sr)
|
||||
self.sp_a.blockSignals(True)
|
||||
self.sp_b.blockSignals(True)
|
||||
self.sp_a.setMaximum(dur)
|
||||
self.sp_b.setMaximum(dur)
|
||||
self.sp_a.setValue(0)
|
||||
self.sp_b.setValue(dur)
|
||||
self.sp_a.blockSignals(False)
|
||||
self.sp_b.blockSignals(False)
|
||||
self._update_new()
|
||||
|
||||
def toggle_record(self):
|
||||
if self.recorder.is_recording():
|
||||
x, sr = self.recorder.stop()
|
||||
self.b_rec.setText("Record")
|
||||
self.b_rec.setIcon(icons.icon("record"))
|
||||
self.meter.setValue(0)
|
||||
if len(x) < sr // 10:
|
||||
self.lbl_rec.setText("nothing recorded")
|
||||
return
|
||||
self.lbl_rec.setText("recorded %s" % fmt_s(len(x) / sr))
|
||||
self.set_source(x, sr, "recording")
|
||||
return
|
||||
name = self.cmb_in.currentText()
|
||||
self.recorder.set_device(find_device(input_devices(), name) if self.cmb_in.currentIndex() > 0 else None)
|
||||
self.ctx.settings["input_device"] = name if self.cmb_in.currentIndex() > 0 else None
|
||||
if self.recorder.start():
|
||||
self.stop()
|
||||
self.b_rec.setText("Stop recording")
|
||||
self.b_rec.setIcon(icons.icon("stop", icons.DANGER))
|
||||
|
||||
def _selection(self, a, b):
|
||||
if b > a:
|
||||
self.sp_a.setValue(a)
|
||||
self.sp_b.setValue(b)
|
||||
|
||||
def trim_silence(self):
|
||||
if not self.src:
|
||||
return
|
||||
x, sr = self.src
|
||||
y = audio.trim_silence(x, rate=sr)
|
||||
self.set_source(y, sr, self.src_name)
|
||||
|
||||
def _changed(self, *_):
|
||||
self._reproc.start()
|
||||
|
||||
def processed(self):
|
||||
"""Replacement audio after trim / gain / normalize / loudness matching -> (x, rate) or None."""
|
||||
if not self.src:
|
||||
return None
|
||||
x, sr = self.src
|
||||
a, b = self.sp_a.value(), self.sp_b.value()
|
||||
if b > a:
|
||||
x = audio.trim(x, sr, a, b)
|
||||
if self.cb_match.isChecked() and self.orig is not None:
|
||||
x = audio.match_loudness(x, self.orig[0])
|
||||
if self.cb_norm.isChecked():
|
||||
x = audio.normalize(x)
|
||||
if self.sp_gain.value():
|
||||
x = audio.gain(x, self.sp_gain.value())
|
||||
return x, sr
|
||||
|
||||
def _update_new(self):
|
||||
p = self.processed()
|
||||
if p is None:
|
||||
self.wf_new.set_audio(None, 1, "load a file or record a replacement")
|
||||
self.lbl_new.setText("")
|
||||
self._warn([])
|
||||
return
|
||||
x, sr = p
|
||||
self.wf_new.set_audio(x, sr)
|
||||
dur = audio.duration(x, sr)
|
||||
s = self.sound
|
||||
self.lbl_new.setText("%s: %.2f s, peak %.1f dBFS" % (self.src_name, dur, audio.peak_db(x)))
|
||||
warnings = []
|
||||
if s and not s.is_pack_media and dur > s.seconds * 1.15 + 0.25:
|
||||
warnings.append("Longer than the original (%.2f s): it will be squeezed into the original's size "
|
||||
"(lower quality) or refused, and the game may cut it off." % s.seconds)
|
||||
if len(x) and np.max(np.abs(x)) >= 0.999:
|
||||
warnings.append("Clipping: lower the gain or use Normalize.")
|
||||
self._warn(warnings)
|
||||
|
||||
def _warn(self, warnings):
|
||||
self.warn_icon.setVisible(bool(warnings))
|
||||
self.lbl_warn.setText(" ".join(warnings))
|
||||
|
||||
# --- playback --------------------------------------------------------------------------------
|
||||
def _audio_for(self, what):
|
||||
return {"orig": self.orig, "inst": self.installed, "new": self.processed()}.get(what)
|
||||
|
||||
def play(self, what, start=0.0):
|
||||
a = self._audio_for(what)
|
||||
if not a:
|
||||
return
|
||||
self.playing_what = what
|
||||
self.ctx.player.play(a[0], a[1], start, loop=self.cb_loop.isChecked(), label=what)
|
||||
|
||||
def toggle_play(self):
|
||||
if self.ctx.player.is_playing():
|
||||
self.stop()
|
||||
else:
|
||||
self.play("new" if self.src else "orig")
|
||||
|
||||
def ab_switch(self):
|
||||
t = self.ctx.player.current_time() if self.ctx.player.is_playing() else 0.0
|
||||
nxt = "orig" if self.playing_what in ("new", "inst") else ("new" if self.src else "inst")
|
||||
if self._audio_for(nxt):
|
||||
self.play(nxt, t)
|
||||
|
||||
def stop(self):
|
||||
self.ctx.player.stop()
|
||||
self._clear_playheads()
|
||||
|
||||
def _clear_playheads(self):
|
||||
for w in (self.wf_orig, self.wf_inst, self.wf_new):
|
||||
w.set_playhead(None)
|
||||
|
||||
def _pos(self, t):
|
||||
self.lbl_time.setText(fmt_s(t))
|
||||
wf = {"orig": self.wf_orig, "inst": self.wf_inst, "new": self.wf_new}.get(self.playing_what)
|
||||
if wf:
|
||||
wf.set_playhead(t)
|
||||
|
||||
# --- apply / revert --------------------------------------------------------------------------
|
||||
def _busy(self, msg):
|
||||
self.busy.setVisible(bool(msg))
|
||||
self.lbl_busy.setText(msg or "")
|
||||
self.b_apply.setEnabled(not msg and self.sound is not None)
|
||||
self.b_revert.setEnabled(not msg and self.sound is not None)
|
||||
|
||||
def _failed(self, what, e):
|
||||
self._busy(None)
|
||||
if isinstance(e, Exception) and str(e) == "cancelled":
|
||||
return
|
||||
self.inform(what, str(e))
|
||||
|
||||
def apply(self, force=False):
|
||||
s = self.sound
|
||||
p = self.processed()
|
||||
if s is None:
|
||||
return
|
||||
if p is None:
|
||||
self.inform("Nothing to apply", "Load an audio file or record a replacement first.")
|
||||
return
|
||||
x, sr = p
|
||||
if not s.is_pack_media and audio.duration(x, sr) > s.seconds * 1.15 + 0.25 and not force:
|
||||
if not self.confirm("Longer than the original",
|
||||
"Your audio is %.2f s, the original %.2f s.\nIt must fit in the original's size, "
|
||||
"so it will be encoded at lower quality (or refused), and the game may cut it "
|
||||
"off.\n\nApply anyway?" % (audio.duration(x, sr), s.seconds)):
|
||||
return
|
||||
ws = self.ctx.ws
|
||||
meta = {"source": self.src_name}
|
||||
|
||||
def work(job):
|
||||
job.report("encoding…")
|
||||
wem, info = ws.prepare(s, x, sr, progress=job.report)
|
||||
job.report("writing game files…")
|
||||
ws.apply([(s, wem, meta)], force=force, progress=job.report)
|
||||
return info
|
||||
|
||||
def done(info):
|
||||
self._busy(None)
|
||||
note = " (shrunk to fit: %d Hz, quality %.1f)" % (info["rate"], info["quality"]) if "shrunk_from" in info else \
|
||||
" (%d Hz, quality %.1f)" % (info["rate"], info["quality"])
|
||||
self.message.emit("Applied %s%s" % (s.name, note))
|
||||
self.applied.emit(s.key)
|
||||
self.set_sound(s)
|
||||
|
||||
def failed(e, tb):
|
||||
if isinstance(e, W.ConflictError):
|
||||
self._busy(None)
|
||||
if self.confirm("Changed outside this tool", str(e) + "\n\nOverwrite it with your replacement?"):
|
||||
self.apply(force=True)
|
||||
return
|
||||
self._failed("Could not apply", e)
|
||||
|
||||
self._busy("encoding…")
|
||||
self.ctx.runner.start(work, done, failed, lambda m: self.lbl_busy.setText(m))
|
||||
|
||||
def revert(self):
|
||||
s = self.sound
|
||||
if s is None:
|
||||
return
|
||||
ws = self.ctx.ws
|
||||
|
||||
def work(job):
|
||||
return ws.revert([s], progress=job.report)
|
||||
|
||||
def done(r):
|
||||
self._busy(None)
|
||||
self.message.emit("Reverted %s to the original" % s.name if r else "%s is already original" % s.name)
|
||||
self.reverted.emit(s.key)
|
||||
self.set_sound(s)
|
||||
|
||||
self._busy("reverting…")
|
||||
self.ctx.runner.start(work, done, lambda e, tb: self._failed("Could not revert", e))
|
||||
|
||||
def export_wav(self):
|
||||
if not self.orig:
|
||||
return
|
||||
which = self.processed() if self.src else self.orig
|
||||
p = self.ask_save(os.path.join(self.ctx.settings.get("last_dir") or "", self.sound.name + ".wav"))
|
||||
if p:
|
||||
audio.save_wav(p, which[0], which[1])
|
||||
self.message.emit("Saved %s" % p)
|
||||
187
app/icons.py
Normal file
187
app/icons.py
Normal file
@@ -0,0 +1,187 @@
|
||||
"""Vector icons drawn with QPainter (one consistent style, no image files, no emoji).
|
||||
|
||||
`icon(name, color)` returns a QIcon rendered at several sizes. Names: play, stop, record, open, apply, revert,
|
||||
save, export, import, settings, star, star-off, warning, ab, loop, search, wave.
|
||||
`app_icon()` uses the artwork in app/assets (made by tools/make_assets.py from the user's images).
|
||||
"""
|
||||
import math
|
||||
import os
|
||||
|
||||
from PySide6.QtCore import QPointF, QRectF, Qt
|
||||
from PySide6.QtGui import QColor, QIcon, QPainter, QPainterPath, QPen, QPixmap
|
||||
|
||||
ACCENT = "#b0242c"
|
||||
STEEL = "#c4c4c4"
|
||||
TEXT = "#e6e6e6"
|
||||
DANGER = "#e0533d"
|
||||
MUTED = "#9a9a9a"
|
||||
|
||||
_cache = {}
|
||||
|
||||
|
||||
def _arrow_arc(p, r, start, span, head=True):
|
||||
rect = QRectF(-r, -r, 2 * r, 2 * r)
|
||||
path = QPainterPath()
|
||||
path.arcMoveTo(rect, start)
|
||||
path.arcTo(rect, start, span)
|
||||
p.drawPath(path)
|
||||
if head:
|
||||
a = math.radians(start + span)
|
||||
tip = QPointF(r * math.cos(a), -r * math.sin(a))
|
||||
d = 1 if span > 0 else -1
|
||||
tang = math.radians(start + span + 90 * d)
|
||||
back = QPointF(math.cos(tang), -math.sin(tang))
|
||||
nrm = QPointF(math.cos(a), -math.sin(a))
|
||||
L = r * 0.55
|
||||
p.drawLine(tip, tip - back * L + nrm * L * 0.6)
|
||||
p.drawLine(tip, tip - back * L - nrm * L * 0.6)
|
||||
|
||||
|
||||
def _draw(p, name, c, s):
|
||||
"""Draw icon `name` in a box of size s (origin top-left)."""
|
||||
pen = QPen(QColor(c), max(1.2, s * 0.09), Qt.PenStyle.SolidLine, Qt.PenCapStyle.RoundCap, Qt.PenJoinStyle.RoundJoin)
|
||||
p.setPen(pen)
|
||||
p.setBrush(Qt.BrushStyle.NoBrush)
|
||||
m = s * 0.18
|
||||
if name == "play":
|
||||
path = QPainterPath()
|
||||
path.moveTo(s * 0.30, s * 0.20)
|
||||
path.lineTo(s * 0.80, s * 0.50)
|
||||
path.lineTo(s * 0.30, s * 0.80)
|
||||
path.closeSubpath()
|
||||
p.setBrush(QColor(c))
|
||||
p.drawPath(path)
|
||||
elif name == "stop":
|
||||
p.setBrush(QColor(c))
|
||||
p.drawRoundedRect(QRectF(s * 0.26, s * 0.26, s * 0.48, s * 0.48), s * 0.06, s * 0.06)
|
||||
elif name == "record":
|
||||
p.setPen(Qt.PenStyle.NoPen)
|
||||
p.setBrush(QColor(DANGER))
|
||||
p.drawEllipse(QRectF(s * 0.22, s * 0.22, s * 0.56, s * 0.56))
|
||||
elif name == "open":
|
||||
path = QPainterPath()
|
||||
path.moveTo(m, s * 0.30)
|
||||
path.lineTo(s * 0.40, s * 0.30)
|
||||
path.lineTo(s * 0.48, s * 0.38)
|
||||
path.lineTo(s - m, s * 0.38)
|
||||
path.lineTo(s - m, s * 0.78)
|
||||
path.lineTo(m, s * 0.78)
|
||||
path.closeSubpath()
|
||||
p.drawPath(path)
|
||||
elif name == "apply":
|
||||
path = QPainterPath()
|
||||
path.moveTo(s * 0.20, s * 0.52)
|
||||
path.lineTo(s * 0.42, s * 0.74)
|
||||
path.lineTo(s * 0.82, s * 0.28)
|
||||
p.drawPath(path)
|
||||
elif name in ("revert", "loop"):
|
||||
p.translate(s / 2, s / 2)
|
||||
if name == "revert":
|
||||
_arrow_arc(p, s * 0.30, 60, 270)
|
||||
else:
|
||||
_arrow_arc(p, s * 0.30, 20, 150)
|
||||
_arrow_arc(p, s * 0.30, 200, 150)
|
||||
p.translate(-s / 2, -s / 2)
|
||||
elif name == "save": # disk
|
||||
path = QPainterPath()
|
||||
path.moveTo(m, m)
|
||||
path.lineTo(s * 0.70, m)
|
||||
path.lineTo(s - m, s * 0.30)
|
||||
path.lineTo(s - m, s - m)
|
||||
path.lineTo(m, s - m)
|
||||
path.closeSubpath()
|
||||
p.drawPath(path)
|
||||
p.drawRect(QRectF(s * 0.32, m, s * 0.30, s * 0.20))
|
||||
p.drawRect(QRectF(s * 0.30, s * 0.56, s * 0.40, s * 0.26))
|
||||
elif name in ("export", "import"):
|
||||
p.drawLine(QPointF(m, s * 0.62), QPointF(m, s - m))
|
||||
p.drawLine(QPointF(m, s - m), QPointF(s - m, s - m))
|
||||
p.drawLine(QPointF(s - m, s - m), QPointF(s - m, s * 0.62))
|
||||
up = name == "export"
|
||||
a, b = (s * 0.66, s * 0.16) if up else (s * 0.16, s * 0.66)
|
||||
p.drawLine(QPointF(s / 2, a), QPointF(s / 2, b))
|
||||
d = -1 if up else 1
|
||||
p.drawLine(QPointF(s / 2, b), QPointF(s / 2 - s * 0.16, b - d * s * 0.16))
|
||||
p.drawLine(QPointF(s / 2, b), QPointF(s / 2 + s * 0.16, b - d * s * 0.16))
|
||||
elif name == "settings":
|
||||
p.translate(s / 2, s / 2)
|
||||
path = QPainterPath()
|
||||
teeth = 8
|
||||
for i in range(teeth * 2):
|
||||
a = math.pi * i / teeth
|
||||
r = s * (0.36 if i % 2 == 0 else 0.27)
|
||||
pt = QPointF(r * math.cos(a), r * math.sin(a))
|
||||
path.moveTo(pt) if i == 0 else path.lineTo(pt)
|
||||
path.closeSubpath()
|
||||
p.drawPath(path)
|
||||
p.drawEllipse(QPointF(0, 0), s * 0.11, s * 0.11)
|
||||
p.translate(-s / 2, -s / 2)
|
||||
elif name in ("star", "star-off"):
|
||||
path = QPainterPath()
|
||||
for i in range(10):
|
||||
a = -math.pi / 2 + math.pi * i / 5
|
||||
r = s * (0.40 if i % 2 == 0 else 0.17)
|
||||
pt = QPointF(s / 2 + r * math.cos(a), s * 0.54 + r * math.sin(a))
|
||||
path.moveTo(pt) if i == 0 else path.lineTo(pt)
|
||||
path.closeSubpath()
|
||||
if name == "star":
|
||||
p.setBrush(QColor(c))
|
||||
p.drawPath(path)
|
||||
elif name == "warning":
|
||||
path = QPainterPath()
|
||||
path.moveTo(s / 2, s * 0.14)
|
||||
path.lineTo(s * 0.88, s * 0.84)
|
||||
path.lineTo(s * 0.12, s * 0.84)
|
||||
path.closeSubpath()
|
||||
p.drawPath(path)
|
||||
p.drawLine(QPointF(s / 2, s * 0.40), QPointF(s / 2, s * 0.60))
|
||||
p.drawPoint(QPointF(s / 2, s * 0.72))
|
||||
elif name == "ab": # compare: half-filled circle
|
||||
rect = QRectF(s * 0.18, s * 0.18, s * 0.64, s * 0.64)
|
||||
half = QPainterPath()
|
||||
half.moveTo(s / 2, s * 0.18)
|
||||
half.arcTo(rect, 90, 180)
|
||||
half.closeSubpath()
|
||||
p.setBrush(QColor(c))
|
||||
p.drawPath(half)
|
||||
p.setBrush(Qt.BrushStyle.NoBrush)
|
||||
p.drawEllipse(rect)
|
||||
elif name == "search":
|
||||
p.drawEllipse(QRectF(s * 0.18, s * 0.18, s * 0.46, s * 0.46))
|
||||
p.drawLine(QPointF(s * 0.58, s * 0.58), QPointF(s * 0.84, s * 0.84))
|
||||
elif name == "wave": # fallback app glyph: waveform bars
|
||||
p.setPen(Qt.PenStyle.NoPen)
|
||||
p.setBrush(QColor(c))
|
||||
for i, h in enumerate((0.25, 0.5, 0.8, 0.45, 0.65, 0.3)):
|
||||
p.drawRoundedRect(QRectF(s * (0.14 + i * 0.13), s * (0.5 - h / 2), s * 0.08, s * h), s * 0.03, s * 0.03)
|
||||
|
||||
|
||||
def icon(name, color=TEXT):
|
||||
key = (name, color)
|
||||
if key not in _cache:
|
||||
ic = QIcon()
|
||||
for sz in (16, 20, 24, 32, 48, 64, 128, 256):
|
||||
pm = QPixmap(sz, sz)
|
||||
pm.fill(Qt.GlobalColor.transparent)
|
||||
p = QPainter(pm)
|
||||
p.setRenderHint(QPainter.RenderHint.Antialiasing)
|
||||
_draw(p, name, color, sz)
|
||||
p.end()
|
||||
ic.addPixmap(pm)
|
||||
_cache[key] = ic
|
||||
return _cache[key]
|
||||
|
||||
|
||||
ASSETS = os.path.join(os.path.dirname(os.path.abspath(__file__)), "assets")
|
||||
|
||||
|
||||
def asset(name):
|
||||
p = os.path.join(ASSETS, name)
|
||||
return p if os.path.exists(p) else None
|
||||
|
||||
|
||||
def app_icon():
|
||||
"""The app icon (Arkham City logo bat sending out sound arcs); a plain waveform glyph if the assets are missing."""
|
||||
p = asset("app_icon.ico") or asset("app_icon.png")
|
||||
return QIcon(p) if p else icon("wave", ACCENT)
|
||||
|
||||
91
app/main.py
Normal file
91
app/main.py
Normal file
@@ -0,0 +1,91 @@
|
||||
"""Unheard City - entry point.
|
||||
|
||||
UnheardCity [--game DIR] start the editor
|
||||
UnheardCity --smoke-test [--game DIR] headless self-test of the bundled pipeline (no window, never
|
||||
writes the game: injects into a temporary copy of a WAD)
|
||||
UnheardCity --quit-after 5 --screenshot shot.png (start, wait, screenshot, quit; for build checks)
|
||||
"""
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
def smoke_test(game_dir=None, report=None):
|
||||
"""Prove the packaged app can decode, encode and inject without any external program."""
|
||||
lines = []
|
||||
|
||||
def print(*a): # noqa: A001 - also captured into the report (windowed builds have no console)
|
||||
lines.append(" ".join(str(x) for x in a))
|
||||
if sys.stdout:
|
||||
sys.stdout.write(lines[-1] + "\n")
|
||||
if report:
|
||||
with open(report, "w", encoding="utf-8") as f:
|
||||
f.write("\n".join(lines) + "\n")
|
||||
|
||||
import shutil
|
||||
import tempfile
|
||||
import numpy as np
|
||||
from ackit import audio, settings
|
||||
from ackit.game import Game, default_game_dir
|
||||
from ackit.inject import inject, read_target
|
||||
from ackit.workspace import Workspace
|
||||
gd = game_dir or default_game_dir(settings.load())
|
||||
print("ffmpeg on PATH:", shutil.which("ffmpeg"), "| vgmstream on PATH:", shutil.which("vgmstream-cli"))
|
||||
if not gd:
|
||||
print("no game found; testing the codec only")
|
||||
from ackit.encoder import encode
|
||||
t = np.arange(48000) / 48000.0
|
||||
x = (0.3 * np.sin(2 * np.pi * 440 * t)).astype(np.float32)[:, None]
|
||||
wem, info = encode(x, 48000, 24000, 1)
|
||||
y, sr = audio.decode_wem(wem)
|
||||
print("codec OK: %d bytes, decoded %.2f s at %d Hz" % (len(wem), len(y) / sr, sr))
|
||||
return 0
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
ws = Workspace(Game(gd), store_root=os.path.join(td, "store"))
|
||||
s = next(x for x in ws.catalog.sounds if x.group == "Speech" and 1.0 < x.seconds < 4.0)
|
||||
x, sr = audio.decode_wem(ws.original_bytes(s))
|
||||
wem, info = ws.prepare(s, x[::-1].copy(), sr)
|
||||
out = os.path.join(td, os.path.basename(ws.wad_path(s)))
|
||||
inject(ws.wad_path(s), {s.target: wem}, {s.target: ws.limit(s)}, out_path=out)
|
||||
assert read_target(out, s.target) == wem
|
||||
print("pipeline OK: %s (%s), %d sounds loaded, re-encoded %d bytes (limit %d), injected into a temp copy"
|
||||
% (s.name, s.archive, len(ws.catalog.sounds), len(wem), ws.limit(s)))
|
||||
return 0
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
ap = argparse.ArgumentParser(prog="UnheardCity")
|
||||
ap.add_argument("--game", help="Arkham City folder")
|
||||
ap.add_argument("--smoke-test", action="store_true")
|
||||
ap.add_argument("--quit-after", type=float)
|
||||
ap.add_argument("--screenshot")
|
||||
ap.add_argument("--report", help="write the smoke-test output to this file")
|
||||
a, rest = ap.parse_known_args(argv)
|
||||
if a.smoke_test:
|
||||
return smoke_test(a.game, a.report)
|
||||
|
||||
from PySide6.QtCore import QTimer
|
||||
from PySide6.QtWidgets import QApplication
|
||||
from ackit import settings as settings_mod
|
||||
from . import icons, theme
|
||||
from .mainwindow import MainWindow
|
||||
|
||||
app = QApplication([sys.argv[0]] + rest)
|
||||
app.setApplicationName("Unheard City")
|
||||
app.setOrganizationName("UnheardCity")
|
||||
app.setWindowIcon(icons.app_icon())
|
||||
st = settings_mod.load()
|
||||
theme.apply(app)
|
||||
w = MainWindow(settings=st, game_dir=a.game)
|
||||
w.show()
|
||||
if a.quit_after:
|
||||
def finish():
|
||||
if a.screenshot:
|
||||
w.grab().save(a.screenshot)
|
||||
app.quit()
|
||||
QTimer.singleShot(int(a.quit_after * 1000), finish)
|
||||
return app.exec()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
640
app/mainwindow.py
Normal file
640
app/mainwindow.py
Normal file
@@ -0,0 +1,640 @@
|
||||
"""Main window: filters + sound table (browser) on top, single-sound editor below."""
|
||||
import os
|
||||
|
||||
from PySide6.QtCore import QByteArray, QEvent, QItemSelectionModel, QSize, Qt, QTimer
|
||||
from PySide6.QtGui import QFont, QKeySequence, QShortcut
|
||||
from PySide6.QtWidgets import (QAbstractItemView, QCheckBox, QComboBox, QDialog, QFileDialog, QFormLayout,
|
||||
QHBoxLayout, QHeaderView, QLabel, QLineEdit, QMainWindow, QMenu, QMessageBox,
|
||||
QPushButton, QScrollArea, QSplitter, QTableView, QToolBar, QVBoxLayout, QWidget)
|
||||
|
||||
from ackit import settings as settings_mod, workspace as W
|
||||
from ackit.game import Game, GameError, default_game_dir
|
||||
from ackit.modpack import export_mod, match_mod, prepare_import
|
||||
from . import icons, theme
|
||||
from .audioio import Player, find_device, output_devices
|
||||
from .dialogs import ExportModDialog, ImportModDialog, SettingsDialog, pick_game_dir
|
||||
from .editor import EditorPanel
|
||||
from .models import C_FAV, C_NAME, COLS, SoundFilter, SoundModel
|
||||
from .spinner import Spinner
|
||||
from .userdata import UserData
|
||||
from .workers import Runner
|
||||
|
||||
SIDEBAR_MIN, SIDEBAR_MAX = 220, 320
|
||||
TABLE_MIN = 160 # the sound list never gets smaller than this when fitting the editor
|
||||
|
||||
ANY = "Any"
|
||||
|
||||
|
||||
class MainWindow(QMainWindow):
|
||||
def __init__(self, settings=None, runner=None, game_dir=None):
|
||||
super().__init__()
|
||||
self.setWindowTitle("Unheard City")
|
||||
self.settings = settings if settings is not None else settings_mod.load()
|
||||
self.runner = runner or Runner()
|
||||
self.player = Player(self)
|
||||
self.player.error.connect(lambda m: self.statusBar().showMessage(m, 5000))
|
||||
self.userdata = UserData()
|
||||
self.ws = None
|
||||
self.loading = False
|
||||
self._build()
|
||||
self._apply_output_device()
|
||||
geo = self.settings.get("window_geometry")
|
||||
if geo:
|
||||
self.restoreGeometry(QByteArray.fromHex(geo.encode()))
|
||||
else:
|
||||
self.resize(1400, 900)
|
||||
gd = game_dir or default_game_dir(self.settings)
|
||||
if gd:
|
||||
QTimer.singleShot(0, lambda: self.load_game(gd, self.settings.get("language")))
|
||||
else:
|
||||
self.show_no_game()
|
||||
|
||||
# --- UI ---------------------------------------------------------------------------------------
|
||||
def _build(self):
|
||||
self.setWindowIcon(icons.app_icon())
|
||||
tb = QToolBar("Main")
|
||||
tb.setMovable(False)
|
||||
tb.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextBesideIcon)
|
||||
tb.setIconSize(QSize(18, 18))
|
||||
self.addToolBar(tb)
|
||||
logo = QLabel()
|
||||
logo.setPixmap(icons.app_icon().pixmap(28, 28))
|
||||
tb.addWidget(logo)
|
||||
title = QLabel("UNHEARD CITY")
|
||||
title.setObjectName("AppTitle")
|
||||
f = title.font()
|
||||
f.setBold(True)
|
||||
f.setLetterSpacing(QFont.SpacingType.AbsoluteSpacing, 1.6)
|
||||
title.setFont(f)
|
||||
tb.addWidget(title)
|
||||
tb.addSeparator()
|
||||
self.cmb_lang = QComboBox()
|
||||
self.cmb_lang.setToolTip("Speech language archive to browse")
|
||||
self.cmb_lang.activated.connect(lambda _: self.load_game(self.ws.game.root, self.cmb_lang.currentText())
|
||||
if self.ws else None)
|
||||
tb.addWidget(QLabel(" Language "))
|
||||
tb.addWidget(self.cmb_lang)
|
||||
tb.addSeparator()
|
||||
self.act_revert_all = tb.addAction(icons.icon("revert"), "Revert all", self.revert_all)
|
||||
self.act_revert_all.setToolTip("Put every original sound back")
|
||||
tb.addAction(icons.icon("export"), "Export mod…", self.export_mod_dialog)
|
||||
tb.addAction(icons.icon("import"), "Import mod…", self.import_mod_dialog)
|
||||
tb.addSeparator()
|
||||
tb.addAction(icons.icon("settings"), "Settings…", self.settings_dialog)
|
||||
|
||||
# banner for problems (changed files, missing game)
|
||||
self.banner = QWidget()
|
||||
self.banner.setObjectName("Banner")
|
||||
self.banner.setAttribute(Qt.WidgetAttribute.WA_StyledBackground, True)
|
||||
bl = QHBoxLayout(self.banner)
|
||||
bl.setContentsMargins(8, 4, 8, 4)
|
||||
warn = QLabel()
|
||||
warn.setPixmap(icons.icon("warning", icons.ACCENT).pixmap(20, 20))
|
||||
bl.addWidget(warn, 0, Qt.AlignmentFlag.AlignTop)
|
||||
self.banner_text = QLabel()
|
||||
self.banner_text.setWordWrap(True)
|
||||
self.b_reapply = QPushButton("Re-apply my changes")
|
||||
self.b_forget = QPushButton("Forget them")
|
||||
self.b_choose = QPushButton("Choose game folder…")
|
||||
self.b_reapply.clicked.connect(self.reapply_lost)
|
||||
self.b_forget.clicked.connect(self.forget_problems)
|
||||
self.b_choose.clicked.connect(self.choose_game)
|
||||
bl.addWidget(self.banner_text, 1)
|
||||
for b in (self.b_reapply, self.b_forget, self.b_choose):
|
||||
bl.addWidget(b)
|
||||
self.banner.hide()
|
||||
|
||||
# filters
|
||||
side = theme.Sidebar()
|
||||
side.configure(self.settings)
|
||||
self.sidebar = side
|
||||
side.setMinimumWidth(SIDEBAR_MIN)
|
||||
side.setMaximumWidth(SIDEBAR_MAX) # opens at the maximum (wallpaper fully visible); can only shrink
|
||||
fl = QFormLayout(side)
|
||||
self.search = QLineEdit()
|
||||
self.search.setPlaceholderText("Search names, characters, notes… (Ctrl+F)")
|
||||
self.search.setClearButtonEnabled(True)
|
||||
self.search.addAction(icons.icon("search", icons.MUTED), QLineEdit.ActionPosition.LeadingPosition)
|
||||
self._search_timer = QTimer(self)
|
||||
self._search_timer.setSingleShot(True)
|
||||
self._search_timer.setInterval(120)
|
||||
self._search_timer.timeout.connect(lambda: self._filter_text())
|
||||
self.search.textChanged.connect(lambda _: self._search_timer.start())
|
||||
self.cmb_group = QComboBox()
|
||||
self.cmb_cat = QComboBox()
|
||||
self.cmb_char = QComboBox()
|
||||
self.cmb_char.setMaxVisibleItems(25)
|
||||
self.cb_inferred = QCheckBox("Include characters mentioned in names")
|
||||
self.cmb_arch = QComboBox()
|
||||
self.cb_mod = QCheckBox("Modified only")
|
||||
self.cb_fav = QCheckBox("Favorites only")
|
||||
self.cmb_tag = QComboBox()
|
||||
clear = QPushButton("Clear filters")
|
||||
clear.setIcon(icons.icon("revert", icons.MUTED))
|
||||
clear.clicked.connect(self.clear_filters)
|
||||
fl.addRow(self.search)
|
||||
fl.addRow("Group", self.cmb_group)
|
||||
fl.addRow("Category", self.cmb_cat)
|
||||
fl.addRow("Character", self.cmb_char)
|
||||
fl.addRow(self.cb_inferred)
|
||||
fl.addRow("Archive", self.cmb_arch)
|
||||
fl.addRow("Tag", self.cmb_tag)
|
||||
fl.addRow(self.cb_mod)
|
||||
fl.addRow(self.cb_fav)
|
||||
fl.addRow(clear)
|
||||
self.lbl_count = QLabel()
|
||||
fl.addRow(self.lbl_count)
|
||||
self.cmb_group.currentTextChanged.connect(self._group_changed)
|
||||
for w in (self.cmb_cat, self.cmb_arch, self.cmb_tag):
|
||||
w.currentTextChanged.connect(lambda _: self._filters_changed())
|
||||
self.cmb_char.currentTextChanged.connect(lambda _: self._filters_changed())
|
||||
for w in (self.cb_inferred, self.cb_mod, self.cb_fav):
|
||||
w.toggled.connect(lambda _: self._filters_changed())
|
||||
|
||||
# table
|
||||
self.model = SoundModel(self)
|
||||
self.proxy = SoundFilter(self)
|
||||
self.proxy.setSourceModel(self.model)
|
||||
self.table = QTableView()
|
||||
self.table.setModel(self.proxy)
|
||||
self.table.setSortingEnabled(True)
|
||||
self.table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
|
||||
self.table.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)
|
||||
self.table.verticalHeader().hide()
|
||||
self.table.verticalHeader().setDefaultSectionSize(22)
|
||||
self.table.setAlternatingRowColors(True)
|
||||
self.table.setWordWrap(False)
|
||||
h = self.table.horizontalHeader()
|
||||
h.setSectionResizeMode(QHeaderView.ResizeMode.Interactive)
|
||||
h.setStretchLastSection(True)
|
||||
for c, w in zip(range(len(COLS)), (28, 360, 140, 200, 70, 55, 110, 75, 70, 200)):
|
||||
self.table.setColumnWidth(c, w)
|
||||
self.table.sortByColumn(C_NAME, Qt.SortOrder.AscendingOrder)
|
||||
self.table.selectionModel().currentRowChanged.connect(self._current_changed)
|
||||
self.table.clicked.connect(self._clicked)
|
||||
self.table.doubleClicked.connect(lambda _: self.editor.play("orig"))
|
||||
self.table.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
|
||||
self.table.customContextMenuRequested.connect(self._context_menu)
|
||||
|
||||
# editor
|
||||
self.editor = EditorPanel(self)
|
||||
self.editor.applied.connect(self._after_change)
|
||||
self.editor.reverted.connect(self._after_change)
|
||||
self.editor.message.connect(lambda m: self.statusBar().showMessage(m, 8000))
|
||||
scroll = QScrollArea()
|
||||
scroll.setWidget(self.editor)
|
||||
scroll.setWidgetResizable(True)
|
||||
|
||||
self.vsplit = QSplitter(Qt.Orientation.Vertical)
|
||||
self.vsplit.addWidget(self.table)
|
||||
self.vsplit.addWidget(scroll)
|
||||
self.vsplit.setStretchFactor(0, 1) # extra height goes to the list; the editor keeps its fitted height
|
||||
self.vsplit.setStretchFactor(1, 0)
|
||||
self.scroll = scroll
|
||||
self._fitted = False
|
||||
self._user_sized = False # once the divider is dragged, keep the user's choice
|
||||
self.vsplit.splitterMoved.connect(lambda *_: setattr(self, "_user_sized", True))
|
||||
self.editor.installEventFilter(self)
|
||||
hs = QSplitter(Qt.Orientation.Horizontal)
|
||||
hs.addWidget(side)
|
||||
hs.addWidget(self.vsplit)
|
||||
hs.setStretchFactor(1, 1)
|
||||
hs.setSizes([SIDEBAR_MAX, 100000])
|
||||
central = QWidget()
|
||||
cl = QVBoxLayout(central)
|
||||
cl.setContentsMargins(0, 0, 0, 0)
|
||||
cl.setSpacing(0)
|
||||
cl.addWidget(self.banner)
|
||||
cl.addWidget(hs, 1)
|
||||
self.setCentralWidget(central)
|
||||
self.spinner = Spinner(14)
|
||||
self.statusBar().addWidget(self.spinner)
|
||||
self.lbl_game = QLabel()
|
||||
self.statusBar().addPermanentWidget(self.lbl_game)
|
||||
|
||||
sc = Qt.ShortcutContext.WindowShortcut
|
||||
QShortcut(QKeySequence("Ctrl+F"), self, self.search.setFocus, context=sc)
|
||||
QShortcut(QKeySequence("Ctrl+S"), self, lambda: self.editor.apply(), context=sc)
|
||||
QShortcut(QKeySequence("Ctrl+O"), self, lambda: self.editor.load_file_dialog(), context=sc)
|
||||
QShortcut(QKeySequence("Escape"), self, self.editor.stop, context=sc)
|
||||
QShortcut(QKeySequence(Qt.Key.Key_Space), self.table, self.editor.toggle_play,
|
||||
context=Qt.ShortcutContext.WidgetShortcut)
|
||||
QShortcut(QKeySequence(Qt.Key.Key_F2), self.table, self.edit_notes, context=Qt.ShortcutContext.WidgetShortcut)
|
||||
|
||||
# --- dialogs (overridable in tests) -----------------------------------------------------------
|
||||
def confirm(self, title, text):
|
||||
return QMessageBox.question(self, title, text) == QMessageBox.StandardButton.Yes
|
||||
|
||||
def inform(self, title, text):
|
||||
QMessageBox.information(self, title, text)
|
||||
|
||||
# --- game loading -----------------------------------------------------------------------------
|
||||
def show_no_game(self, msg=None):
|
||||
self.banner_text.setText(msg or "Batman: Arkham City was not found automatically. Choose the game folder "
|
||||
"(it contains BmGame and Binaries; on Linux usually ~/.steam/steam/steamapps/"
|
||||
"common/Batman Arkham City GOTY).")
|
||||
self.b_reapply.hide()
|
||||
self.b_forget.hide()
|
||||
self.b_choose.show()
|
||||
self.banner.show()
|
||||
|
||||
def choose_game(self):
|
||||
d = pick_game_dir(self, self.settings.get("game_dir") or "")
|
||||
if d:
|
||||
self.load_game(d)
|
||||
|
||||
def load_game(self, game_dir, language=None):
|
||||
try:
|
||||
game = Game(game_dir)
|
||||
except GameError as e:
|
||||
self.show_no_game(str(e))
|
||||
return
|
||||
lang = language if language in game.languages else game.language()
|
||||
self.loading = True
|
||||
self.statusBar().showMessage("Loading sound list…")
|
||||
self.spinner.show()
|
||||
store = self.settings.get("store_root")
|
||||
full = self.settings.get("full_backup", True)
|
||||
|
||||
def work(job):
|
||||
return W.Workspace(game, store, [lang, "SFX"] if "SFX" in game.archives else [lang], full_backup=full,
|
||||
progress=lambda n, t: job.report("scanning %s: %d / %d" % (lang, n, t)))
|
||||
|
||||
def done(ws):
|
||||
self.loading = False
|
||||
self.spinner.hide()
|
||||
self.ws = ws
|
||||
self.settings["game_dir"] = game.root
|
||||
self.settings["language"] = lang
|
||||
self.save_settings()
|
||||
self.cmb_lang.blockSignals(True)
|
||||
self.cmb_lang.clear()
|
||||
self.cmb_lang.addItems(game.languages)
|
||||
self.cmb_lang.setCurrentText(lang)
|
||||
self.cmb_lang.blockSignals(False)
|
||||
self.populate()
|
||||
self.banner.hide()
|
||||
self.lbl_game.setText("%s • store: %s" % (game.root, ws.store.root))
|
||||
self.statusBar().showMessage("Loaded %d sounds (%s)." % (len(ws.catalog.sounds), ", ".join(ws.catalog.archives)), 6000)
|
||||
self.editor.refresh_devices()
|
||||
self.check_problems()
|
||||
|
||||
def failed(e, tb):
|
||||
self.loading = False
|
||||
self.spinner.hide()
|
||||
self.show_no_game("Could not load the game audio: %s" % e)
|
||||
|
||||
self.runner.start(work, done, failed, lambda m: self.statusBar().showMessage(m))
|
||||
|
||||
def populate(self):
|
||||
ws = self.ws
|
||||
self.model.reset(ws.catalog.sounds, ws.modified_keys(), self.userdata)
|
||||
snd = ws.catalog.sounds
|
||||
|
||||
def fill(cmb, items):
|
||||
cur = cmb.currentText()
|
||||
cmb.blockSignals(True)
|
||||
cmb.clear()
|
||||
cmb.addItems([ANY] + items)
|
||||
i = cmb.findText(cur)
|
||||
cmb.setCurrentIndex(i if i >= 0 else 0)
|
||||
cmb.blockSignals(False)
|
||||
|
||||
fill(self.cmb_group, [g for g in ("Speech", "Music", "Ambience", "SFX", "Unknown") if any(s.group == g for s in snd)])
|
||||
self._fill_categories()
|
||||
chars = sorted({c.strip() for s in snd for c in s.character.split(";") if c.strip()})
|
||||
fill(self.cmb_char, chars)
|
||||
fill(self.cmb_arch, list(ws.catalog.archives))
|
||||
fill(self.cmb_tag, self.userdata.all_tags())
|
||||
self._filters_changed()
|
||||
|
||||
def _fill_categories(self):
|
||||
g = self.cmb_group.currentText()
|
||||
cats = sorted({s.category for s in (self.ws.catalog.sounds if self.ws else []) if g == ANY or s.group == g})
|
||||
cur = self.cmb_cat.currentText()
|
||||
self.cmb_cat.blockSignals(True)
|
||||
self.cmb_cat.clear()
|
||||
self.cmb_cat.addItems([ANY] + cats)
|
||||
i = self.cmb_cat.findText(cur)
|
||||
self.cmb_cat.setCurrentIndex(i if i >= 0 else 0)
|
||||
self.cmb_cat.blockSignals(False)
|
||||
|
||||
# --- filtering --------------------------------------------------------------------------------
|
||||
def _val(self, cmb):
|
||||
t = cmb.currentText().strip()
|
||||
return None if not t or t == ANY else t
|
||||
|
||||
def _group_changed(self, _):
|
||||
self._fill_categories()
|
||||
self._filters_changed()
|
||||
|
||||
def _filters_changed(self):
|
||||
self.proxy.set(group=self._val(self.cmb_group), category=self._val(self.cmb_cat),
|
||||
character=self._val(self.cmb_char), include_inferred=self.cb_inferred.isChecked(),
|
||||
archive=self._val(self.cmb_arch), modified_only=self.cb_mod.isChecked(),
|
||||
favorites_only=self.cb_fav.isChecked(), tag=self._val(self.cmb_tag))
|
||||
self._update_count()
|
||||
|
||||
def _filter_text(self):
|
||||
self.proxy.set_text(self.search.text())
|
||||
self._update_count()
|
||||
|
||||
def _update_count(self):
|
||||
self.lbl_count.setText("%d of %d sounds" % (self.proxy.rowCount(), self.model.rowCount()))
|
||||
|
||||
def clear_filters(self):
|
||||
self.search.clear()
|
||||
for c in (self.cmb_group, self.cmb_cat, self.cmb_char, self.cmb_arch, self.cmb_tag):
|
||||
c.setCurrentIndex(0)
|
||||
for c in (self.cb_inferred, self.cb_mod, self.cb_fav):
|
||||
c.setChecked(False)
|
||||
self._filter_text()
|
||||
|
||||
# --- selection --------------------------------------------------------------------------------
|
||||
def selected_sounds(self):
|
||||
rows = self.table.selectionModel().selectedRows()
|
||||
return [self.proxy.sound_at(i) for i in rows]
|
||||
|
||||
def _current_changed(self, cur, prev):
|
||||
if cur.isValid():
|
||||
self.editor.set_sound(self.proxy.sound_at(cur))
|
||||
|
||||
def _clicked(self, index):
|
||||
if index.column() == C_FAV:
|
||||
s = self.proxy.sound_at(index)
|
||||
self.userdata.update(s.key, fav=not self.userdata.favorite(s.key))
|
||||
self.model.refresh_keys()
|
||||
|
||||
def select_name(self, name):
|
||||
"""Select a sound by name (used by tests and 'jump to')."""
|
||||
for r in range(self.proxy.rowCount()):
|
||||
idx = self.proxy.index(r, C_NAME)
|
||||
if self.proxy.sound_at(idx).name == name:
|
||||
self.table.selectionModel().setCurrentIndex(
|
||||
idx, QItemSelectionModel.SelectionFlag.ClearAndSelect | QItemSelectionModel.SelectionFlag.Rows)
|
||||
self.table.scrollTo(idx)
|
||||
return True
|
||||
return False
|
||||
|
||||
def _context_menu(self, pos):
|
||||
sel = self.selected_sounds()
|
||||
if not sel:
|
||||
return
|
||||
m = QMenu(self)
|
||||
m.addAction("Play original", lambda: self.editor.play("orig"))
|
||||
m.addAction("Notes && tags… (F2)", self.edit_notes)
|
||||
m.addAction("Toggle favorite", lambda: self._toggle_fav(sel))
|
||||
m.addSeparator()
|
||||
m.addAction("Revert selected (%d)" % len(sel), lambda: self.revert_sounds(sel))
|
||||
m.addAction("Copy name", lambda: self._copy("\n".join(s.name for s in sel)))
|
||||
m.exec(self.table.viewport().mapToGlobal(pos))
|
||||
|
||||
def _copy(self, text):
|
||||
from PySide6.QtWidgets import QApplication
|
||||
QApplication.clipboard().setText(text)
|
||||
|
||||
def _toggle_fav(self, sounds):
|
||||
on = not all(self.userdata.favorite(s.key) for s in sounds)
|
||||
for s in sounds:
|
||||
self.userdata.update(s.key, fav=on)
|
||||
self.model.refresh_keys()
|
||||
|
||||
def edit_notes(self, sounds=None, note=None, tags=None):
|
||||
sounds = sounds or self.selected_sounds()
|
||||
if not sounds:
|
||||
return
|
||||
s0 = sounds[0]
|
||||
if note is None:
|
||||
dlg = QDialog(self)
|
||||
dlg.setWindowTitle("Notes & tags - %s" % (s0.name if len(sounds) == 1 else "%d sounds" % len(sounds)))
|
||||
f = QFormLayout(dlg)
|
||||
en = QLineEdit(self.userdata.note(s0.key))
|
||||
et = QLineEdit(", ".join(self.userdata.tags(s0.key)))
|
||||
et.setPlaceholderText("comma separated, e.g. funny, todo")
|
||||
f.addRow("Note", en)
|
||||
f.addRow("Tags", et)
|
||||
from PySide6.QtWidgets import QDialogButtonBox
|
||||
bb = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
|
||||
bb.accepted.connect(dlg.accept)
|
||||
bb.rejected.connect(dlg.reject)
|
||||
f.addRow(bb)
|
||||
dlg.resize(460, 0)
|
||||
if dlg.exec() != QDialog.DialogCode.Accepted:
|
||||
return
|
||||
note, tags = en.text(), [t for t in et.text().split(",")]
|
||||
for s in sounds:
|
||||
self.userdata.update(s.key, note=note, tags=tags)
|
||||
self.model.refresh_keys()
|
||||
cur = self.cmb_tag.currentText()
|
||||
self.cmb_tag.blockSignals(True)
|
||||
self.cmb_tag.clear()
|
||||
self.cmb_tag.addItems([ANY] + self.userdata.all_tags())
|
||||
self.cmb_tag.setCurrentText(cur)
|
||||
self.cmb_tag.blockSignals(False)
|
||||
|
||||
# --- changes ----------------------------------------------------------------------------------
|
||||
def _after_change(self, key=None):
|
||||
self.model.refresh_keys(modified=self.ws.modified_keys())
|
||||
self.proxy.refilter()
|
||||
self._update_count()
|
||||
|
||||
def revert_sounds(self, sounds):
|
||||
sounds = [s for s in sounds if self.ws.store.get(s.key)]
|
||||
if not sounds:
|
||||
self.statusBar().showMessage("Nothing to revert: the selected sounds are original.", 5000)
|
||||
return
|
||||
self._run_change("Reverting %d sound(s)…" % len(sounds), lambda job: self.ws.revert(sounds, job.report),
|
||||
"Reverted %d sound(s)." % len(sounds))
|
||||
|
||||
def revert_all(self):
|
||||
if not self.ws:
|
||||
return
|
||||
n = len(self.ws.store.entries)
|
||||
if not n:
|
||||
self.inform("Revert all", "All sounds are already original.")
|
||||
return
|
||||
if not self.confirm("Revert all", "Put the original back for all %d changed sound(s)?" % n):
|
||||
return
|
||||
self._run_change("Reverting everything…", lambda job: self.ws.revert_all(job.report),
|
||||
"All sounds are original again.")
|
||||
|
||||
def _run_change(self, busy, fn, ok_msg):
|
||||
self.statusBar().showMessage(busy)
|
||||
self.spinner.show()
|
||||
|
||||
def done(_):
|
||||
self.spinner.hide()
|
||||
self.statusBar().showMessage(ok_msg, 8000)
|
||||
self._after_change()
|
||||
if self.editor.sound:
|
||||
self.editor.set_sound(self.editor.sound)
|
||||
self.check_problems()
|
||||
|
||||
def failed(e, tb):
|
||||
self.spinner.hide()
|
||||
self.inform("Error", str(e))
|
||||
|
||||
self.runner.start(fn, done, failed, lambda m: self.statusBar().showMessage(m))
|
||||
|
||||
# --- changed underneath -----------------------------------------------------------------------
|
||||
def check_problems(self):
|
||||
ws = self.ws
|
||||
if not ws:
|
||||
return
|
||||
|
||||
def work(job):
|
||||
out = []
|
||||
for key in list(ws.store.entries):
|
||||
s = ws.catalog.by_key.get(key)
|
||||
if s is None:
|
||||
continue
|
||||
st = ws.state(s)
|
||||
if st in (W.LOST, W.CHANGED):
|
||||
out.append((s, st))
|
||||
return out
|
||||
|
||||
def done(problems):
|
||||
self.problems = problems
|
||||
if not problems:
|
||||
if self.banner.isVisible() and not self.b_choose.isVisible():
|
||||
self.banner.hide()
|
||||
return
|
||||
lost = [s.name for s, st in problems if st == W.LOST]
|
||||
changed = [s.name for s, st in problems if st == W.CHANGED]
|
||||
txt = []
|
||||
if lost:
|
||||
txt.append("%d of your changes were undone outside this editor (Steam 'verify files' or an update?): %s"
|
||||
% (len(lost), ", ".join(lost[:4]) + (" …" if len(lost) > 4 else "")))
|
||||
if changed:
|
||||
txt.append("%d sound(s) were changed by something else since you modified them: %s. "
|
||||
"Revert puts the original back." % (len(changed), ", ".join(changed[:4])))
|
||||
self.banner_text.setText("\n".join(txt))
|
||||
self.b_reapply.show()
|
||||
self.b_forget.show()
|
||||
self.b_choose.hide()
|
||||
self.banner.show()
|
||||
|
||||
self.runner.start(work, done)
|
||||
|
||||
def reapply_lost(self):
|
||||
ws = self.ws
|
||||
items = [(s, ws.store.get_blob(ws.store.get(s.key)["applied"]), ws.store.get(s.key).get("meta", {}))
|
||||
for s, st in getattr(self, "problems", []) if ws.store.get(s.key) and ws.store.get(s.key).get("applied")]
|
||||
if items:
|
||||
self._run_change("Re-applying…", lambda job: ws.apply(items, force=True, progress=job.report),
|
||||
"Re-applied %d change(s)." % len(items))
|
||||
|
||||
def forget_problems(self):
|
||||
if not self.confirm("Forget", "Stop tracking these sounds? Their current game files are kept as they are."):
|
||||
return
|
||||
for s, _ in getattr(self, "problems", []):
|
||||
self.ws.store.forget(s.key)
|
||||
self.ws.store.save()
|
||||
self.ws.store.gc()
|
||||
self.banner.hide()
|
||||
self._after_change()
|
||||
|
||||
# --- mods -------------------------------------------------------------------------------------
|
||||
def export_mod_dialog(self, path=None, keys=None, title="", author="", description=""):
|
||||
if not self.ws:
|
||||
return
|
||||
mods = self.ws.modified_keys()
|
||||
if not mods:
|
||||
self.inform("Export mod", "You have not changed any sound yet.")
|
||||
return
|
||||
if keys is None:
|
||||
names = [(k, self.ws.catalog.by_key[k].name if k in self.ws.catalog.by_key else k) for k in mods]
|
||||
dlg = ExportModDialog(names, self)
|
||||
if dlg.exec() != QDialog.DialogCode.Accepted:
|
||||
return
|
||||
keys, title, author, description = (dlg.selected_keys(), dlg.title.text(), dlg.author.text(),
|
||||
dlg.desc.toPlainText())
|
||||
path, _ = QFileDialog.getSaveFileName(self, "Save mod package", os.path.join(
|
||||
self.settings.get("last_dir") or "", (title or "my_mod") + ".acmod"), "Unheard City mod (*.acmod)")
|
||||
if not path:
|
||||
return
|
||||
self._run_change("Exporting…", lambda job: export_mod(self.ws, path, keys, title, author, description),
|
||||
"Saved %s" % path)
|
||||
|
||||
def import_mod_dialog(self, path=None, confirm=True):
|
||||
if not self.ws:
|
||||
return
|
||||
if path is None:
|
||||
path, _ = QFileDialog.getOpenFileName(self, "Install mod package", self.settings.get("last_dir") or "",
|
||||
"Unheard City mod (*.acmod)")
|
||||
if not path:
|
||||
return
|
||||
try:
|
||||
man, items = match_mod(self.ws, path)
|
||||
except Exception as e: # noqa: BLE001
|
||||
self.inform("Import mod", "Cannot read this package: %s" % e)
|
||||
return
|
||||
if confirm and ImportModDialog(man, items, self).exec() != QDialog.DialogCode.Accepted:
|
||||
return
|
||||
ws = self.ws
|
||||
|
||||
def work(job):
|
||||
prepare_import(ws, path, items, progress=job.report)
|
||||
changes = [(im.sound, im.wem, {"source": "mod:" + os.path.basename(path)}) for im in items if im.wem]
|
||||
try:
|
||||
ws.apply(changes, progress=job.report)
|
||||
except W.ConflictError:
|
||||
ws.apply(changes, force=True, progress=job.report)
|
||||
return len(changes)
|
||||
|
||||
self._run_change("Installing mod…", work, "Installed %s" % os.path.basename(path))
|
||||
|
||||
# --- settings ---------------------------------------------------------------------------------
|
||||
def settings_dialog(self):
|
||||
dlg = SettingsDialog(self.settings, self)
|
||||
if dlg.exec() != QDialog.DialogCode.Accepted:
|
||||
return
|
||||
old = dict(self.settings)
|
||||
self.settings.update(dlg.values())
|
||||
self.save_settings()
|
||||
self._apply_output_device()
|
||||
self.sidebar.configure(self.settings)
|
||||
if (self.settings.get("game_dir") != old.get("game_dir") or self.settings.get("store_root") != old.get("store_root")
|
||||
or self.settings.get("full_backup") != old.get("full_backup")):
|
||||
if self.settings.get("game_dir"):
|
||||
self.load_game(self.settings["game_dir"], self.settings.get("language"))
|
||||
|
||||
def _apply_output_device(self):
|
||||
name = self.settings.get("output_device")
|
||||
self.player.set_device(find_device(output_devices(), name) if name else None)
|
||||
|
||||
def save_settings(self):
|
||||
self.settings["window_geometry"] = bytes(self.saveGeometry().toHex()).decode()
|
||||
try:
|
||||
settings_mod.save(self.settings)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
def fit_editor(self):
|
||||
"""Give the editor panel the height of its contents (everything visible), the list gets the rest."""
|
||||
total = sum(self.vsplit.sizes()) or self.vsplit.height()
|
||||
lay = self.editor.layout()
|
||||
need = self.editor.sizeHint().height()
|
||||
if lay.hasHeightForWidth(): # wrapped labels: height at the actual width
|
||||
need = max(need, lay.heightForWidth(self.scroll.viewport().width()))
|
||||
need += 2 * self.scroll.frameWidth()
|
||||
if self.editor.minimumSizeHint().width() > self.scroll.viewport().width(): # narrow window: room for the h-bar
|
||||
need += self.scroll.horizontalScrollBar().sizeHint().height()
|
||||
editor_h = min(need, max(0, total - TABLE_MIN))
|
||||
self.vsplit.setSizes([total - editor_h, editor_h])
|
||||
|
||||
def eventFilter(self, obj, e):
|
||||
if obj is self.editor and e.type() == QEvent.Type.LayoutRequest and self._fitted and not self._user_sized:
|
||||
QTimer.singleShot(0, self.fit_editor)
|
||||
return super().eventFilter(obj, e)
|
||||
|
||||
def showEvent(self, e):
|
||||
super().showEvent(e)
|
||||
if not self._fitted:
|
||||
self._fitted = True
|
||||
QTimer.singleShot(0, self.fit_editor)
|
||||
|
||||
def closeEvent(self, e):
|
||||
self.player.stop()
|
||||
self.save_settings()
|
||||
super().closeEvent(e)
|
||||
184
app/models.py
Normal file
184
app/models.py
Normal file
@@ -0,0 +1,184 @@
|
||||
"""Table model for the sound browser + a combined filter (search text and facet filters)."""
|
||||
from PySide6.QtCore import QAbstractTableModel, QModelIndex, QSortFilterProxyModel, Qt
|
||||
from PySide6.QtGui import QFont, QPalette
|
||||
from PySide6.QtWidgets import QApplication
|
||||
|
||||
from . import icons
|
||||
|
||||
COLS = ["", "Name", "Category", "Character", "Length", "Archive", "Format", "Max size", "Status", "Notes / tags"]
|
||||
C_FAV, C_NAME, C_CAT, C_CHAR, C_LEN, C_ARCH, C_FMT, C_SIZE, C_STATUS, C_NOTES = range(len(COLS))
|
||||
SORT_ROLE = Qt.ItemDataRole.UserRole + 1
|
||||
SOUND_ROLE = Qt.ItemDataRole.UserRole + 2
|
||||
|
||||
|
||||
class SoundModel(QAbstractTableModel):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.sounds = []
|
||||
self.modified = set()
|
||||
self.userdata = None
|
||||
self.hay = []
|
||||
|
||||
def reset(self, sounds, modified, userdata):
|
||||
self.beginResetModel()
|
||||
self.sounds, self.modified, self.userdata = list(sounds), set(modified), userdata
|
||||
self._build_hay()
|
||||
self.endResetModel()
|
||||
|
||||
def _build_hay(self):
|
||||
ud = self.userdata
|
||||
self.hay = ["\n".join((s.name, s.category, s.character, s.characters_inferred,
|
||||
ud.note(s.key) if ud else "", " ".join(ud.tags(s.key)) if ud else "")).lower()
|
||||
for s in self.sounds]
|
||||
self.ids = [s.target.replace(":", " ").replace("/", " ").lower() for s in self.sounds]
|
||||
|
||||
def refresh_keys(self, keys=None, modified=None):
|
||||
"""Update status / notes of some rows (all if keys is None)."""
|
||||
if modified is not None:
|
||||
self.modified = set(modified)
|
||||
self._build_hay()
|
||||
if self.sounds:
|
||||
self.dataChanged.emit(self.index(0, 0), self.index(len(self.sounds) - 1, len(COLS) - 1))
|
||||
|
||||
def rowCount(self, parent=QModelIndex()):
|
||||
return 0 if parent.isValid() else len(self.sounds)
|
||||
|
||||
def columnCount(self, parent=QModelIndex()):
|
||||
return len(COLS)
|
||||
|
||||
def headerData(self, section, orientation, role=Qt.ItemDataRole.DisplayRole):
|
||||
if orientation == Qt.Orientation.Horizontal and role == Qt.ItemDataRole.DisplayRole:
|
||||
return COLS[section]
|
||||
if orientation == Qt.Orientation.Horizontal and role == Qt.ItemDataRole.DecorationRole and section == C_FAV:
|
||||
return icons.icon("star", icons.ACCENT)
|
||||
if orientation == Qt.Orientation.Horizontal and role == Qt.ItemDataRole.ToolTipRole:
|
||||
return {C_FAV: "Favorite", C_CHAR: "Verified speaker; ~grey = characters mentioned by the name",
|
||||
C_SIZE: "Largest replacement that fits (speech/effects keep the original's size)",
|
||||
C_STATUS: "Modified = contains your replacement"}.get(section)
|
||||
return None
|
||||
|
||||
def data(self, index, role=Qt.ItemDataRole.DisplayRole):
|
||||
if not index.isValid():
|
||||
return None
|
||||
s = self.sounds[index.row()]
|
||||
c = index.column()
|
||||
ud = self.userdata
|
||||
if role == SOUND_ROLE:
|
||||
return s
|
||||
if role == Qt.ItemDataRole.DisplayRole:
|
||||
if c == C_FAV:
|
||||
return ""
|
||||
if c == C_NAME:
|
||||
return s.name
|
||||
if c == C_CAT:
|
||||
return s.category
|
||||
if c == C_CHAR:
|
||||
return s.character or (("~ " + s.characters_inferred) if s.characters_inferred else "")
|
||||
if c == C_LEN:
|
||||
return "%.2f s" % s.seconds
|
||||
if c == C_ARCH:
|
||||
return s.archive
|
||||
if c == C_FMT:
|
||||
return "%d Hz %s" % (s.rate, {1: "mono", 2: "stereo"}.get(s.channels, "%d ch" % s.channels))
|
||||
if c == C_SIZE:
|
||||
return "any" if s.is_pack_media else "%.1f KB" % (s.size / 1024.0)
|
||||
if c == C_STATUS:
|
||||
return "Modified" if s.key in self.modified else ""
|
||||
if c == C_NOTES:
|
||||
if not ud:
|
||||
return ""
|
||||
t = ud.tags(s.key)
|
||||
return " ".join("#" + x for x in t) + (" " if t else "") + ud.note(s.key)
|
||||
if role == SORT_ROLE:
|
||||
if c == C_FAV:
|
||||
return 0 if ud and ud.favorite(s.key) else 1
|
||||
if c == C_LEN:
|
||||
return s.seconds
|
||||
if c == C_SIZE:
|
||||
return 1e12 if s.is_pack_media else s.size
|
||||
if c == C_FMT:
|
||||
return (s.rate, s.channels)
|
||||
if c == C_STATUS:
|
||||
return 0 if s.key in self.modified else 1
|
||||
return (self.data(index, Qt.ItemDataRole.DisplayRole) or "").lower()
|
||||
if role == Qt.ItemDataRole.DecorationRole and c == C_FAV:
|
||||
fav = bool(ud and ud.favorite(s.key))
|
||||
return icons.icon("star" if fav else "star-off", icons.ACCENT if fav else "#4a4a4a")
|
||||
if role == Qt.ItemDataRole.DecorationRole and c == C_STATUS and s.key in self.modified:
|
||||
return icons.icon("apply", icons.ACCENT)
|
||||
if role == Qt.ItemDataRole.ForegroundRole and c == C_CHAR and not s.character:
|
||||
return QApplication.palette().color(QPalette.ColorRole.PlaceholderText)
|
||||
if role == Qt.ItemDataRole.FontRole and s.key in self.modified and c in (C_NAME, C_STATUS):
|
||||
f = QFont()
|
||||
f.setBold(True)
|
||||
return f
|
||||
if role == Qt.ItemDataRole.ToolTipRole and c in (C_NAME, C_CHAR):
|
||||
tip = "%s\n%s %s" % (s.name, s.key, s.group + " / " + s.category)
|
||||
if s.character:
|
||||
tip += "\nSpeaker: " + s.character
|
||||
if s.characters_inferred:
|
||||
tip += "\nMentioned in the name: " + s.characters_inferred
|
||||
return tip
|
||||
return None
|
||||
|
||||
|
||||
class SoundFilter(QSortFilterProxyModel):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.words = []
|
||||
self.group = None
|
||||
self.category = None
|
||||
self.character = None
|
||||
self.include_inferred = False
|
||||
self.archive = None
|
||||
self.modified_only = False
|
||||
self.favorites_only = False
|
||||
self.tag = None
|
||||
self.setSortRole(SORT_ROLE)
|
||||
self.setDynamicSortFilter(False)
|
||||
|
||||
def refilter(self):
|
||||
if hasattr(self, "beginFilterChange"): # Qt >= 6.10
|
||||
self.beginFilterChange()
|
||||
self.endFilterChange(QSortFilterProxyModel.Direction.Rows)
|
||||
else:
|
||||
self.invalidateRowsFilter()
|
||||
|
||||
def set(self, **kw):
|
||||
for k, v in kw.items():
|
||||
setattr(self, k, v)
|
||||
self.refilter()
|
||||
|
||||
def set_text(self, text):
|
||||
self.words = [w for w in text.lower().replace("_", " ").split() if w]
|
||||
self.refilter()
|
||||
|
||||
def filterAcceptsRow(self, row, parent):
|
||||
m = self.sourceModel()
|
||||
s = m.sounds[row]
|
||||
if self.group and s.group != self.group:
|
||||
return False
|
||||
if self.category and s.category != self.category:
|
||||
return False
|
||||
if self.archive and s.archive != self.archive:
|
||||
return False
|
||||
if self.character:
|
||||
c = self.character.lower()
|
||||
if c not in s.character.lower() and not (self.include_inferred and c in s.characters_inferred.lower()):
|
||||
return False
|
||||
if self.modified_only and s.key not in m.modified:
|
||||
return False
|
||||
ud = m.userdata
|
||||
if self.favorites_only and not (ud and ud.favorite(s.key)):
|
||||
return False
|
||||
if self.tag and not (ud and self.tag in ud.tags(s.key)):
|
||||
return False
|
||||
if self.words:
|
||||
h = m.hay[row]
|
||||
for w in self.words:
|
||||
if w not in h and not (len(w) >= 6 and w in m.ids[row]): # IDs like b49cdf48 also work
|
||||
return False
|
||||
return True
|
||||
|
||||
def sound_at(self, proxy_index):
|
||||
return self.sourceModel().sounds[self.mapToSource(proxy_index).row()]
|
||||
53
app/spinner.py
Normal file
53
app/spinner.py
Normal file
@@ -0,0 +1,53 @@
|
||||
"""Small activity spinner: a rotating accent-coloured arc. Runs only while visible."""
|
||||
from PySide6.QtCore import QRectF, QTimer, Qt
|
||||
from PySide6.QtGui import QColor, QPainter, QPen
|
||||
from PySide6.QtWidgets import QWidget
|
||||
|
||||
from . import icons
|
||||
|
||||
STEP_MS = 16
|
||||
STEP_DEG = 9
|
||||
|
||||
|
||||
def paint_arc(p, rect, angle, color=icons.ACCENT):
|
||||
"""Draw the spinner (faint ring + 100 degree arc) into rect; used by Spinner and the waveform overlay."""
|
||||
w = max(1.5, rect.width() / 8)
|
||||
r = rect.adjusted(w / 2, w / 2, -w / 2, -w / 2)
|
||||
p.save()
|
||||
p.setRenderHint(QPainter.RenderHint.Antialiasing)
|
||||
ring = QColor(color)
|
||||
ring.setAlpha(45)
|
||||
p.setPen(QPen(ring, w))
|
||||
p.drawEllipse(r)
|
||||
pen = QPen(QColor(color), w)
|
||||
pen.setCapStyle(Qt.PenCapStyle.RoundCap)
|
||||
p.setPen(pen)
|
||||
p.drawArc(r, int(-angle * 16), 100 * 16)
|
||||
p.restore()
|
||||
|
||||
|
||||
class Spinner(QWidget):
|
||||
def __init__(self, size=16, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setFixedSize(size, size)
|
||||
self.angle = 0
|
||||
self.timer = QTimer(self)
|
||||
self.timer.setInterval(STEP_MS)
|
||||
self.timer.timeout.connect(self._step)
|
||||
self.hide()
|
||||
|
||||
def _step(self):
|
||||
self.angle = (self.angle + STEP_DEG) % 360
|
||||
self.update()
|
||||
|
||||
def showEvent(self, e):
|
||||
self.timer.start()
|
||||
super().showEvent(e)
|
||||
|
||||
def hideEvent(self, e):
|
||||
self.timer.stop()
|
||||
super().hideEvent(e)
|
||||
|
||||
def paintEvent(self, _):
|
||||
p = QPainter(self)
|
||||
paint_arc(p, QRectF(self.rect()), self.angle)
|
||||
140
app/theme.py
Normal file
140
app/theme.py
Normal file
@@ -0,0 +1,140 @@
|
||||
"""The app look: neutral dark greys, one dark red accent, the wallpaper behind the filter panel.
|
||||
|
||||
`apply(app)` sets the style for the whole application; `Sidebar` is the filter panel, painted over the wallpaper
|
||||
(app/assets/wallpaper.jpg) when the background is on.
|
||||
"""
|
||||
from PySide6.QtCore import Qt
|
||||
from PySide6.QtGui import QColor, QImage, QPainter, QPalette
|
||||
from PySide6.QtWidgets import QWidget
|
||||
|
||||
from . import icons
|
||||
|
||||
ACCENT = "#b0242c"
|
||||
ACCENT_HI = "#cf3a42"
|
||||
STEEL = "#c4c4c4" # the original waveform: neutral light grey
|
||||
TEXT = "#e6e6e6"
|
||||
MUTED = "#9a9a9a"
|
||||
BG = "#141414" # dialogs
|
||||
PANEL_BG = "#0c0c0c" # main window: behind the filter and editor panels (darker for contrast)
|
||||
SURFACE = "#1b1b1b"
|
||||
BORDER = "#2c2c2c"
|
||||
CONTROL = "#3a3a3a"
|
||||
|
||||
QSS = """
|
||||
QWidget { color: %(TEXT)s; }
|
||||
QMainWindow { background: %(PANEL_BG)s; }
|
||||
QDialog { background: %(BG)s; }
|
||||
QToolTip { background: #202020; color: %(TEXT)s; border: 1px solid %(CONTROL)s; padding: 4px; }
|
||||
QToolBar { background: #0f0f0f; border: none; border-bottom: 1px solid %(BORDER)s; spacing: 6px; padding: 4px 6px; }
|
||||
QToolBar QToolButton { color: %(TEXT)s; padding: 4px 9px; border-radius: 4px; }
|
||||
QToolBar QToolButton:hover { background: #262626; }
|
||||
QLabel#AppTitle { color: %(TEXT)s; font-weight: bold; padding: 0 10px 0 2px; }
|
||||
QStatusBar { background: #0f0f0f; color: %(MUTED)s; border-top: 1px solid %(BORDER)s; }
|
||||
QStatusBar QLabel { color: %(MUTED)s; }
|
||||
QWidget#Sidebar { background: transparent; border-right: 1px solid %(BORDER)s; }
|
||||
QWidget#EditorPanel, QScrollArea, QScrollArea > QWidget > QWidget { background: transparent; }
|
||||
QWidget#Banner { background: #3a1416; border-bottom: 1px solid %(ACCENT)s; }
|
||||
QLabel#EditorTitle { color: %(TEXT)s; }
|
||||
QLabel#Muted { color: %(MUTED)s; }
|
||||
QTableView { background: %(SURFACE)s; alternate-background-color: #202020; gridline-color: %(BORDER)s;
|
||||
border: 1px solid %(BORDER)s; selection-background-color: rgba(176,36,44,70); selection-color: #ffffff; }
|
||||
QTableView::item { padding: 0 4px; }
|
||||
QHeaderView::section { background: #171717; color: %(MUTED)s; border: none; border-right: 1px solid %(BORDER)s;
|
||||
border-bottom: 1px solid %(CONTROL)s; padding: 4px 6px; font-weight: bold; }
|
||||
QTableCornerButton::section { background: #171717; border: none; }
|
||||
QGroupBox { background: %(SURFACE)s; border: 1px solid %(BORDER)s; border-radius: 6px;
|
||||
margin-top: 16px; padding: 10px 6px 6px 6px; }
|
||||
QGroupBox::title { subcontrol-origin: margin; left: 12px; padding: 0 6px; color: %(MUTED)s; font-weight: bold; }
|
||||
QPushButton { background: #242424; border: 1px solid %(CONTROL)s; border-radius: 4px; padding: 5px 12px; }
|
||||
QPushButton:hover { background: #2c2c2c; border-color: #4a4a4a; }
|
||||
QPushButton:pressed, QPushButton:checked { background: rgba(176,36,44,45); border-color: %(ACCENT)s; }
|
||||
QPushButton:disabled { color: #5c5c5c; border-color: #2a2a2a; background: #1c1c1c; }
|
||||
QPushButton#Primary { background: %(ACCENT)s; color: #ffffff; font-weight: bold; border: 1px solid %(ACCENT)s; }
|
||||
QPushButton#Primary:hover { background: %(ACCENT_HI)s; border-color: %(ACCENT_HI)s; }
|
||||
QPushButton#Primary:disabled { background: #3a1c1e; color: #8a6466; border-color: #3a1c1e; }
|
||||
QLineEdit, QComboBox, QDoubleSpinBox, QPlainTextEdit, QListWidget, QTreeWidget {
|
||||
background: #101010; border: 1px solid %(CONTROL)s; border-radius: 4px; padding: 3px 6px;
|
||||
selection-background-color: rgba(176,36,44,120); }
|
||||
QLineEdit:focus, QComboBox:focus, QDoubleSpinBox:focus, QPlainTextEdit:focus { border-color: %(ACCENT)s; }
|
||||
QComboBox QAbstractItemView { background: #1b1b1b; border: 1px solid %(CONTROL)s;
|
||||
selection-background-color: rgba(176,36,44,90); }
|
||||
QCheckBox::indicator { width: 14px; height: 14px; border: 1px solid #555555; border-radius: 3px; background: #101010; }
|
||||
QCheckBox::indicator:checked { background: %(ACCENT)s; border-color: %(ACCENT)s; }
|
||||
QProgressBar { background: #101010; border: 1px solid %(CONTROL)s; border-radius: 3px; height: 10px; }
|
||||
QProgressBar::chunk { background: %(ACCENT)s; border-radius: 2px; }
|
||||
QSlider::groove:horizontal { height: 4px; background: #333333; border-radius: 2px; }
|
||||
QSlider::sub-page:horizontal { background: %(ACCENT)s; border-radius: 2px; }
|
||||
QSlider::handle:horizontal { background: %(TEXT)s; width: 12px; margin: -5px 0; border-radius: 6px; }
|
||||
QSplitter::handle { background: %(BORDER)s; }
|
||||
QScrollBar:vertical, QScrollBar:horizontal { background: transparent; border: none; margin: 0; }
|
||||
QScrollBar:vertical { width: 10px; } QScrollBar:horizontal { height: 10px; }
|
||||
QScrollBar::handle { background: #3c3c3c; border-radius: 4px; min-height: 24px; min-width: 24px; }
|
||||
QScrollBar::handle:hover { background: #555555; }
|
||||
QScrollBar::add-line, QScrollBar::sub-line { width: 0; height: 0; }
|
||||
QMenu { background: #1b1b1b; border: 1px solid %(CONTROL)s; }
|
||||
QMenu::item { padding: 5px 22px 5px 10px; }
|
||||
QMenu::item:selected { background: rgba(176,36,44,70); }
|
||||
""" % dict(TEXT=TEXT, BG=BG, PANEL_BG=PANEL_BG, ACCENT=ACCENT, ACCENT_HI=ACCENT_HI, MUTED=MUTED, BORDER=BORDER, SURFACE=SURFACE,
|
||||
CONTROL=CONTROL)
|
||||
|
||||
# waveform colours: original / installed / replacement
|
||||
WAVE = {"orig": QColor(STEEL), "inst": QColor("#8f4a4e"), "new": QColor(ACCENT)}
|
||||
WAVE_BG = QColor("#111111")
|
||||
SIDEBAR_VEIL = QColor(16, 16, 16) # alpha follows the background strength
|
||||
|
||||
|
||||
def apply(app):
|
||||
app.setStyle("Fusion")
|
||||
pal = QPalette()
|
||||
for role, col in [(QPalette.ColorRole.Window, BG), (QPalette.ColorRole.WindowText, TEXT),
|
||||
(QPalette.ColorRole.Base, "#101010"), (QPalette.ColorRole.AlternateBase, "#202020"),
|
||||
(QPalette.ColorRole.Text, TEXT), (QPalette.ColorRole.Button, "#242424"),
|
||||
(QPalette.ColorRole.ButtonText, TEXT), (QPalette.ColorRole.Highlight, ACCENT),
|
||||
(QPalette.ColorRole.HighlightedText, "#ffffff"), (QPalette.ColorRole.ToolTipBase, "#202020"),
|
||||
(QPalette.ColorRole.ToolTipText, TEXT), (QPalette.ColorRole.PlaceholderText, MUTED),
|
||||
(QPalette.ColorRole.Mid, "#333333"), (QPalette.ColorRole.Link, ACCENT)]:
|
||||
pal.setColor(role, QColor(col))
|
||||
pal.setColor(QPalette.ColorGroup.Disabled, QPalette.ColorRole.ButtonText, QColor("#5c5c5c"))
|
||||
pal.setColor(QPalette.ColorGroup.Disabled, QPalette.ColorRole.Text, QColor("#5c5c5c"))
|
||||
app.setPalette(pal)
|
||||
app.setStyleSheet(QSS)
|
||||
|
||||
|
||||
def wallpaper():
|
||||
"""The Arkham City wallpaper (app/assets/wallpaper.jpg) as a QImage, or None if the assets are missing."""
|
||||
p = icons.asset("wallpaper.jpg")
|
||||
img = QImage(p) if p else QImage()
|
||||
return None if img.isNull() else img
|
||||
|
||||
|
||||
class Sidebar(QWidget):
|
||||
"""Filter panel with the wallpaper behind it: scaled to the panel height, its left part fills the panel."""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setObjectName("Sidebar")
|
||||
self.setAttribute(Qt.WidgetAttribute.WA_StyledBackground, True)
|
||||
self.image = None
|
||||
self.opacity = 0.1
|
||||
self._scaled = None
|
||||
|
||||
def configure(self, settings):
|
||||
st = settings or {}
|
||||
self.image = wallpaper() if st.get("background_enabled", True) else None
|
||||
self.opacity = max(0.0, min(1.0, float(st.get("background_opacity", 0.1))))
|
||||
self._scaled = None
|
||||
self.update()
|
||||
|
||||
def paintEvent(self, e):
|
||||
if self.image is not None:
|
||||
if self._scaled is None or self._scaled.height() != self.height():
|
||||
self._scaled = self.image.scaledToHeight(self.height(), Qt.TransformationMode.SmoothTransformation)
|
||||
p = QPainter(self)
|
||||
p.setOpacity(self.opacity)
|
||||
p.drawImage(0, 0, self._scaled)
|
||||
p.setOpacity(1.0)
|
||||
veil = QColor(SIDEBAR_VEIL)
|
||||
veil.setAlpha(int(180 * self.opacity)) # stronger image, stronger veil: labels stay readable
|
||||
p.fillRect(self.rect(), veil)
|
||||
p.end()
|
||||
super().paintEvent(e)
|
||||
50
app/userdata.py
Normal file
50
app/userdata.py
Normal file
@@ -0,0 +1,50 @@
|
||||
"""Per-user notes, tags and favorites for sounds (user data folder, never the game folder)."""
|
||||
import json
|
||||
import os
|
||||
|
||||
from ackit.paths import user_dir
|
||||
|
||||
|
||||
class UserData:
|
||||
def __init__(self, path=None):
|
||||
self.path = path or os.path.join(user_dir("data"), "sound_notes.json")
|
||||
try:
|
||||
self.data = json.load(open(self.path, encoding="utf-8"))
|
||||
except (OSError, ValueError):
|
||||
self.data = {}
|
||||
|
||||
def save(self):
|
||||
tmp = self.path + ".tmp"
|
||||
with open(tmp, "w", encoding="utf-8") as f:
|
||||
json.dump(self.data, f, indent=1, ensure_ascii=False)
|
||||
os.replace(tmp, self.path)
|
||||
|
||||
def _get(self, key):
|
||||
return self.data.get(key, {})
|
||||
|
||||
def note(self, key):
|
||||
return self._get(key).get("note", "")
|
||||
|
||||
def tags(self, key):
|
||||
return self._get(key).get("tags", [])
|
||||
|
||||
def favorite(self, key):
|
||||
return bool(self._get(key).get("fav"))
|
||||
|
||||
def update(self, key, note=None, tags=None, fav=None):
|
||||
d = dict(self._get(key))
|
||||
if note is not None:
|
||||
d["note"] = note.strip()
|
||||
if tags is not None:
|
||||
d["tags"] = sorted({t.strip() for t in tags if t.strip()})
|
||||
if fav is not None:
|
||||
d["fav"] = bool(fav)
|
||||
d = {k: v for k, v in d.items() if v}
|
||||
if d:
|
||||
self.data[key] = d
|
||||
else:
|
||||
self.data.pop(key, None)
|
||||
self.save()
|
||||
|
||||
def all_tags(self):
|
||||
return sorted({t for d in self.data.values() for t in d.get("tags", [])})
|
||||
123
app/waveform.py
Normal file
123
app/waveform.py
Normal file
@@ -0,0 +1,123 @@
|
||||
"""Waveform display: envelope, playhead, click-to-seek, drag-to-select (trim region)."""
|
||||
import numpy as np
|
||||
from PySide6.QtCore import QPointF, QRectF, Qt, QTimer, Signal
|
||||
from PySide6.QtGui import QColor, QPainter, QPalette, QPen
|
||||
from PySide6.QtWidgets import QSizePolicy, QWidget
|
||||
|
||||
from . import spinner
|
||||
|
||||
|
||||
class Waveform(QWidget):
|
||||
seekRequested = Signal(float) # seconds
|
||||
selectionChanged = Signal(float, float) # seconds (start, end); (0, 0) = none
|
||||
|
||||
def __init__(self, parent=None, selectable=False):
|
||||
super().__init__(parent)
|
||||
self.setMinimumHeight(70)
|
||||
self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred)
|
||||
self.selectable = selectable
|
||||
self.env = None # (mins, maxs) at a fixed resolution
|
||||
self.duration = 0.0
|
||||
self.playhead = None
|
||||
self.sel = None
|
||||
self._drag = None
|
||||
self.placeholder = "no audio"
|
||||
self.accent = None
|
||||
self.bg = None # background colour (theme), else the palette's Base
|
||||
self.busy = False # spinner beside the placeholder (audio being loaded)
|
||||
self._angle = 0
|
||||
self._timer = QTimer(self)
|
||||
self._timer.setInterval(spinner.STEP_MS)
|
||||
self._timer.timeout.connect(self._spin)
|
||||
|
||||
def _spin(self):
|
||||
self._angle = (self._angle + spinner.STEP_DEG) % 360
|
||||
self.update()
|
||||
|
||||
def set_audio(self, x, rate, placeholder="no audio", busy=False):
|
||||
self.placeholder = placeholder
|
||||
self.busy = busy and (x is None or len(x) == 0)
|
||||
self._timer.start() if self.busy else self._timer.stop()
|
||||
self.sel = None
|
||||
self.playhead = None
|
||||
if x is None or len(x) == 0:
|
||||
self.env, self.duration = None, 0.0
|
||||
else:
|
||||
m = x.mean(axis=1) if x.ndim == 2 else x
|
||||
cols = 2000
|
||||
n = len(m)
|
||||
edges = np.linspace(0, n, cols + 1).astype(int)
|
||||
mins = np.array([m[a:b].min() if b > a else 0 for a, b in zip(edges[:-1], edges[1:])])
|
||||
maxs = np.array([m[a:b].max() if b > a else 0 for a, b in zip(edges[:-1], edges[1:])])
|
||||
self.env = (mins, maxs)
|
||||
self.duration = n / float(rate)
|
||||
self.update()
|
||||
|
||||
def set_playhead(self, seconds):
|
||||
self.playhead = seconds
|
||||
self.update()
|
||||
|
||||
def set_selection(self, a, b):
|
||||
self.sel = (min(a, b), max(a, b)) if b != a else None
|
||||
self.update()
|
||||
|
||||
def _x2t(self, x):
|
||||
return max(0.0, min(self.duration, x / max(1, self.width()) * self.duration))
|
||||
|
||||
def _t2x(self, t):
|
||||
return t / self.duration * self.width() if self.duration else 0
|
||||
|
||||
def mousePressEvent(self, e):
|
||||
if not self.duration:
|
||||
return
|
||||
t = self._x2t(e.position().x())
|
||||
if self.selectable and e.button() == Qt.MouseButton.LeftButton and e.modifiers() & Qt.KeyboardModifier.ShiftModifier:
|
||||
self._drag = t
|
||||
elif self.selectable and e.button() == Qt.MouseButton.RightButton:
|
||||
self._drag = t
|
||||
else:
|
||||
self.seekRequested.emit(t)
|
||||
|
||||
def mouseMoveEvent(self, e):
|
||||
if self._drag is not None:
|
||||
self.set_selection(self._drag, self._x2t(e.position().x()))
|
||||
|
||||
def mouseReleaseEvent(self, e):
|
||||
if self._drag is not None:
|
||||
self._drag = None
|
||||
a, b = self.sel if self.sel else (0.0, 0.0)
|
||||
self.selectionChanged.emit(a, b)
|
||||
|
||||
def paintEvent(self, _):
|
||||
p = QPainter(self)
|
||||
pal = self.palette()
|
||||
r = self.rect()
|
||||
p.fillRect(r, self.bg or pal.color(QPalette.ColorRole.Base))
|
||||
mid = r.height() / 2
|
||||
fg = self.accent or pal.color(QPalette.ColorRole.Highlight)
|
||||
if self.env is None:
|
||||
p.setPen(pal.color(QPalette.ColorRole.PlaceholderText))
|
||||
p.drawText(r, Qt.AlignmentFlag.AlignCenter, self.placeholder)
|
||||
if self.busy:
|
||||
tw = p.fontMetrics().horizontalAdvance(self.placeholder)
|
||||
d = 14
|
||||
spinner.paint_arc(p, QRectF(r.center().x() - tw / 2 - d - 8, mid - d / 2, d, d), self._angle)
|
||||
return
|
||||
if self.sel:
|
||||
sc = QColor(fg)
|
||||
sc.setAlpha(50)
|
||||
p.fillRect(QRectF(self._t2x(self.sel[0]), 0, self._t2x(self.sel[1]) - self._t2x(self.sel[0]), r.height()), sc)
|
||||
mins, maxs = self.env
|
||||
w = max(1, r.width())
|
||||
idx = (np.arange(w) * len(mins) / w).astype(int)
|
||||
p.setPen(QPen(fg, 1))
|
||||
for x, i in enumerate(idx):
|
||||
j = min(len(mins), int((x + 1) * len(mins) / w) + 1)
|
||||
lo, hi = mins[i:j].min(), maxs[i:j].max()
|
||||
p.drawLine(QPointF(x, mid - hi * (mid - 2)), QPointF(x, mid - lo * (mid - 2)))
|
||||
p.setPen(QPen(pal.color(QPalette.ColorRole.Mid), 1))
|
||||
p.drawLine(0, int(mid), w, int(mid))
|
||||
if self.playhead is not None:
|
||||
p.setPen(QPen(pal.color(QPalette.ColorRole.Text), 1))
|
||||
x = self._t2x(self.playhead)
|
||||
p.drawLine(QPointF(x, 0), QPointF(x, r.height()))
|
||||
72
app/workers.py
Normal file
72
app/workers.py
Normal file
@@ -0,0 +1,72 @@
|
||||
"""Run long operations off the GUI thread, with progress messages and cancel."""
|
||||
import traceback
|
||||
|
||||
from PySide6.QtCore import QObject, QRunnable, QThreadPool, Signal
|
||||
|
||||
|
||||
class Cancelled(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class _Signals(QObject):
|
||||
done = Signal(object)
|
||||
failed = Signal(object, str)
|
||||
progress = Signal(str)
|
||||
|
||||
|
||||
class Job(QRunnable):
|
||||
"""fn(job) -> result. Inside fn call job.report(msg) and job.check() (raises Cancelled)."""
|
||||
|
||||
def __init__(self, fn):
|
||||
super().__init__()
|
||||
self.fn = fn
|
||||
self.signals = _Signals()
|
||||
self.cancelled = False
|
||||
|
||||
def report(self, msg):
|
||||
self.check()
|
||||
self.signals.progress.emit(str(msg))
|
||||
|
||||
def check(self):
|
||||
if self.cancelled:
|
||||
raise Cancelled()
|
||||
|
||||
def cancel(self):
|
||||
self.cancelled = True
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
r = self.fn(self)
|
||||
except Cancelled:
|
||||
self.signals.failed.emit(Cancelled(), "cancelled")
|
||||
except Exception as e: # noqa: BLE001 - shown to the user
|
||||
self.signals.failed.emit(e, traceback.format_exc())
|
||||
else:
|
||||
self.signals.done.emit(r)
|
||||
|
||||
|
||||
class Runner:
|
||||
"""Starts jobs on a thread pool (or inline when `sync`, used by the GUI tests)."""
|
||||
|
||||
def __init__(self, sync=False):
|
||||
self.sync = sync
|
||||
self.pool = QThreadPool.globalInstance()
|
||||
self.active = set() # keep Python references until the queued result signal is delivered
|
||||
|
||||
def start(self, fn, on_done=None, on_failed=None, on_progress=None):
|
||||
job = Job(fn)
|
||||
job.setAutoDelete(False)
|
||||
self.active.add(job)
|
||||
job.signals.done.connect(lambda *_: self.active.discard(job))
|
||||
job.signals.failed.connect(lambda *_: self.active.discard(job))
|
||||
if on_done:
|
||||
job.signals.done.connect(on_done)
|
||||
if on_failed:
|
||||
job.signals.failed.connect(on_failed)
|
||||
if on_progress:
|
||||
job.signals.progress.connect(on_progress)
|
||||
if self.sync:
|
||||
job.run()
|
||||
else:
|
||||
self.pool.start(job)
|
||||
return job
|
||||
10817
data/catalog/DEU.csv
Normal file
10817
data/catalog/DEU.csv
Normal file
File diff suppressed because it is too large
Load Diff
10817
data/catalog/ESN.csv
Normal file
10817
data/catalog/ESN.csv
Normal file
File diff suppressed because it is too large
Load Diff
10817
data/catalog/FRA.csv
Normal file
10817
data/catalog/FRA.csv
Normal file
File diff suppressed because it is too large
Load Diff
10817
data/catalog/INT.csv
Normal file
10817
data/catalog/INT.csv
Normal file
File diff suppressed because it is too large
Load Diff
10817
data/catalog/ITA.csv
Normal file
10817
data/catalog/ITA.csv
Normal file
File diff suppressed because it is too large
Load Diff
37
data/catalog/README.md
Normal file
37
data/catalog/README.md
Normal file
@@ -0,0 +1,37 @@
|
||||
# Sound catalog
|
||||
|
||||
| file | content |
|
||||
|------|---------|
|
||||
| `<ARCHIVE>.csv` | every sound of one audio archive: `ITA`, `INT`, `FRA`, `DEU`, `ESN` (speech, one per language) and `SFX` (music, ambience, effects) |
|
||||
| `characters.csv` | `name,character`: verified speaker per sound name (language independent) |
|
||||
| `index.json` | fingerprint of the game WAD each CSV was built from (`ackit/game.py: wad_fingerprint`) and counts |
|
||||
| `source/<lang>/*.txt` | **the curated original lists** (one file per character, one wemscan-style file name per line, e.g. `Speech_TAPE_JOKER_1_4.wem`). `Files.txt` = all files, `Stats.txt` = counts, `Checkpoint.txt` = progress marker. Never edited by tools. |
|
||||
|
||||
All CSVs are UTF-8 with a header row. They contain only names and metadata (no game audio).
|
||||
|
||||
## `<ARCHIVE>.csv` columns
|
||||
| column | meaning |
|
||||
|--------|---------|
|
||||
| `archive` | `ITA`, `INT`, ... or `SFX` |
|
||||
| `target` | location inside the archive: `HASH` (entry with one WEM), `HASH:i` (i-th variation, 0-based), `PACK/MEDIA` (music stream pack) |
|
||||
| `name` | display name (`NAME`, `NAME_1..n` for variations, see `ackit/catalog.py`) |
|
||||
| `aliases` | other accepted names, `\|`-separated (`NAME_1` for single entries, `NAME#i` for variations) |
|
||||
| `group` | Speech, Music, Ambience, SFX, Unknown (`docs/categories.md`) |
|
||||
| `category` | finer category (`docs/categories.md`) |
|
||||
| `character` | verified speaker(s) from `characters.csv`, `; `-separated, empty if unknown |
|
||||
| `characters_inferred` | characters/roles mentioned by the name (hint only) |
|
||||
| `seconds`, `rate`, `channels` | of the original sound |
|
||||
| `size` | size of the original WEM in bytes = maximum size of a replacement (except music packs: any size) |
|
||||
| `sha1` | first 16 hex digits of the original WEM's SHA-1 (detects files changed outside the tool) |
|
||||
| `notes` | free text (empty; for future curation, e.g. transcripts) |
|
||||
|
||||
The editor uses these files directly when the installed WAD matches `index.json`; for another game build it
|
||||
scans the WAD itself and applies the same rules (no checksums then).
|
||||
|
||||
## Rebuilding / adding a language
|
||||
1. Make sure the game files are original (`ackit status`; `ackit revert --all`).
|
||||
2. Optional: add listening-based lists for another language in `source/<lang>/<Character>.txt` (same format;
|
||||
names are the same in every language, so the Italian lists already label all languages).
|
||||
3. Run `python tools/build_catalog.py` - it scans every archive of the installed game (all languages present)
|
||||
and rewrites the CSVs, `characters.csv` and `index.json`.
|
||||
New character labels need a display name in `ackit/classify.py: USER_LABELS` (otherwise the file name is used).
|
||||
652
data/catalog/SFX.csv
Normal file
652
data/catalog/SFX.csv
Normal file
@@ -0,0 +1,652 @@
|
||||
archive,target,name,aliases,group,category,character,characters_inferred,seconds,rate,channels,size,sha1,notes
|
||||
SFX,8301A1DA:0,AMB_OW_E8_CH0_1,AMB_OW_E8_CH0#1,Ambience,Ambience,,,26.465,32000,2,347255,d4f1950c2ccb9cbb,
|
||||
SFX,8301A1DA:1,AMB_OW_E8_CH0_2,AMB_OW_E8_CH0#2,Ambience,Ambience,,,26.465,32000,2,341343,f6b9f4d0d90bfbfd,
|
||||
SFX,84CC9D20:0,AMB_Meatpacking_B1_1,AMB_Meatpacking_B1#1,Ambience,Ambience,,,13.819,32000,4,368622,045daf15846206fa,
|
||||
SFX,84CC9D20:1,AMB_Meatpacking_B1_2,AMB_Meatpacking_B1#2,Ambience,Ambience,,,21.382,32000,4,633549,5ba0fbc1f14ad4c3,
|
||||
SFX,84CC9D20:2,AMB_Meatpacking_B1_3,AMB_Meatpacking_B1#3,Ambience,Ambience,,,9.316,32000,4,255018,1b3f4a087588fa8e,
|
||||
SFX,84CC9D20:3,AMB_Meatpacking_B1_4,AMB_Meatpacking_B1#4,Ambience,Ambience,,,15.027,32000,4,398709,9fe235e189ae929c,
|
||||
SFX,84CC9D20:4,AMB_Meatpacking_B1_5,AMB_Meatpacking_B1#5,Ambience,Ambience,,,9.447,32000,4,265375,7b20faec40c234e8,
|
||||
SFX,84CC9D20:5,AMB_Meatpacking_B1_6,AMB_Meatpacking_B1#6,Ambience,Ambience,,,11.576,32000,4,313386,fa901e65bda499a6,
|
||||
SFX,84CC9D20:6,AMB_Meatpacking_B1_7,AMB_Meatpacking_B1#7,Ambience,Ambience,,,25.747,32000,4,689430,b63a0028528493b2,
|
||||
SFX,85F1F884:0,AMB_OW_C257_1,AMB_OW_C257#1,Ambience,Ambience,,,57.597,32000,4,1730923,a0b3071f0df4d576,
|
||||
SFX,85F1F884:1,AMB_OW_C257_2,AMB_OW_C257#2,Ambience,Ambience,,,42.956,32000,4,1283371,fcc511bca155e988,
|
||||
SFX,85F1F884:2,AMB_OW_C257_3,AMB_OW_C257#3,Ambience,Ambience,,,59.559,32000,4,1734273,ace4cf02968e016a,
|
||||
SFX,85F1F884:3,AMB_OW_C257_4,AMB_OW_C257#4,Ambience,Ambience,,,89.000,32000,4,2587371,4b2939c4c48d3a37,
|
||||
SFX,898441C1:0,AMB_OW_Warzone_1,AMB_OW_Warzone#1,Ambience,Ambience,,,30.411,32000,2,393285,c22d04e66ef95442,
|
||||
SFX,898441C1:1,AMB_OW_Warzone_2,AMB_OW_Warzone#2,Ambience,Ambience,,,30.411,32000,2,395301,78fe4e98c9dd26f9,
|
||||
SFX,898441C1:2,AMB_OW_Warzone_3,AMB_OW_Warzone#3,Ambience,Ambience,,,34.441,32000,1,200811,010b705597bb7ecb,
|
||||
SFX,8D6D9BB9:0,AMB_Museum_C1_1,AMB_Museum_C1#1,Ambience,Ambience,,,20.845,32000,4,479172,a32802f6e677bcbf,
|
||||
SFX,8D6D9BB9:1,AMB_Museum_C1_2,AMB_Museum_C1#2,Ambience,Ambience,,,35.837,32000,4,976154,2ff67a7bb43c64e0,
|
||||
SFX,8D6D9BBA,AMB_Museum_C2,AMB_Museum_C2_1,Ambience,Ambience,,,48.049,32000,4,1316154,1b24fce48be812f7,
|
||||
SFX,8E6D9D29,AMB_Museum_B2,AMB_Museum_B2_1,Ambience,Ambience,,,34.557,32000,4,923709,f1ac94349c012b69,
|
||||
SFX,8E6D9D2A,AMB_Museum_B1,AMB_Museum_B1_1,Ambience,Ambience,,,49.087,32000,4,1220981,6e9cf2ec033e68bf,
|
||||
SFX,8F6D9E9F,AMB_Museum_A1,AMB_Museum_A1_1,Ambience,Ambience,,,34.499,32000,4,953199,6eb679248891bd1a,
|
||||
SFX,98A82EFD:0,AMB_Steel_B1_1,AMB_Steel_B1#1,Ambience,Ambience,,,27.477,32000,4,743201,1ed8f1a2af43d06a,
|
||||
SFX,98A82EFD:1,AMB_Steel_B1_2,AMB_Steel_B1#2,Ambience,Ambience,,,6.981,48000,4,258105,82ce38a05064a82a,
|
||||
SFX,98A82EFE,AMB_Steel_B2,AMB_Steel_B2_1,Ambience,Ambience,,,23.790,32000,4,607771,cab42ed9b44112b7,
|
||||
SFX,98A82EFF:0,AMB_Steel_B3_1,AMB_Steel_B3#1,Ambience,Ambience,,,18.323,32000,2,200224,a5366b02aa271e25,
|
||||
SFX,98A82EFF:1,AMB_Steel_B3_2,AMB_Steel_B3#2,Ambience,Ambience,,,28.002,32000,4,693850,4ebb25c954313e04,
|
||||
SFX,98A82EFF:2,AMB_Steel_B3_3,AMB_Steel_B3#3,Ambience,Ambience,,,18.985,32000,2,200757,0ab8c0a668b21772,
|
||||
SFX,998A5D81:0,PSA_Underworld_Groans_1,PSA_Underworld_Groans#1,Ambience,Ambience,,,4.977,24000,1,14907,2abb33cf9e8b4b4f,
|
||||
SFX,998A5D81:1,PSA_Underworld_Groans_2,PSA_Underworld_Groans#2,Ambience,Ambience,,,6.875,24000,1,19276,ecd69d2dce5e37a7,
|
||||
SFX,998A5D81:2,PSA_Underworld_Groans_3,PSA_Underworld_Groans#3,Ambience,Ambience,,,6.591,24000,1,18148,7e09b2d25c2eea3a,
|
||||
SFX,998A5D81:3,PSA_Underworld_Groans_4,PSA_Underworld_Groans#4,Ambience,Ambience,,,4.287,24000,1,13144,bf602fa72cf41dfe,
|
||||
SFX,998A5D81:4,PSA_Underworld_Groans_5,PSA_Underworld_Groans#5,Ambience,Ambience,,,7.211,24000,1,21936,c7a506f8097c8015,
|
||||
SFX,998A5D81:5,PSA_Underworld_Groans_6,PSA_Underworld_Groans#6,Ambience,Ambience,,,3.002,24000,1,9132,09901bea0b7aeb67,
|
||||
SFX,998A5D81:6,PSA_Underworld_Groans_7,PSA_Underworld_Groans#7,Ambience,Ambience,,,4.678,24000,1,14423,de40e62612dad2fd,
|
||||
SFX,998A5D81:7,PSA_Underworld_Groans_8,PSA_Underworld_Groans#8,Ambience,Ambience,,,5.649,24000,1,15919,7161ab2c29c0ad02,
|
||||
SFX,998A5D81:8,PSA_Underworld_Groans_9,PSA_Underworld_Groans#9,Ambience,Ambience,,,7.804,24000,1,22303,e97522fb2216e189,
|
||||
SFX,998A5D81:9,PSA_Underworld_Groans_10,PSA_Underworld_Groans#10,Ambience,Ambience,,,2.753,24000,1,8077,43a3682b07706ee1,
|
||||
SFX,99A8306A:0,AMB_Steel_C5_1,AMB_Steel_C5#1,Ambience,Ambience,,,11.293,32000,4,305185,d41ddb1bea9221b2,
|
||||
SFX,99A8306A:1,AMB_Steel_C5_2,AMB_Steel_C5#2,Ambience,Ambience,,,11.677,32000,4,318256,ab7d96341730b0fb,
|
||||
SFX,99A8306B,AMB_Steel_C4,AMB_Steel_C4_1,Ambience,Ambience,,,14.436,32000,4,402822,f43b23089b800ec3,
|
||||
SFX,99A8306C,AMB_Steel_C3,AMB_Steel_C3_1,Ambience,Ambience,,,7.346,32000,2,79462,deb1ccc91d8b0f26,
|
||||
SFX,99A8306D,AMB_Steel_C2,AMB_Steel_C2_1,Ambience,Ambience,,,25.600,32000,4,684810,706448449f2b4842,
|
||||
SFX,99A8306E,AMB_Steel_C1,AMB_Steel_C1_1,Ambience,Ambience,,,21.419,32000,4,609944,6c7606342cf158d7,
|
||||
SFX,99F8DB33:0,AMB_CW03_1,AMB_CW03#1,Ambience,Ambience,,Catwoman,89.348,32000,4,2424261,77fef44e5c5a8d08,
|
||||
SFX,99F8DB33:1,AMB_CW03_2,AMB_CW03#2,Ambience,Ambience,,Catwoman,9.461,32000,4,220339,50fa995c758b6ea6,
|
||||
SFX,99F8DB33:2,AMB_CW03_3,AMB_CW03#3,Ambience,Ambience,,Catwoman,49.740,32000,4,1349759,78af61a718af1b0b,
|
||||
SFX,99F8DB33:3,AMB_CW03_4,AMB_CW03#4,Ambience,Ambience,,Catwoman,30.336,48000,4,902577,aaa3b73a68f71eb7,
|
||||
SFX,99F8DB33:4,AMB_CW03_5,AMB_CW03#5,Ambience,Ambience,,Catwoman,14.012,32000,4,381734,d9e66d2f055f9dbd,
|
||||
SFX,99F8DB33:5,AMB_CW03_6,AMB_CW03#6,Ambience,Ambience,,Catwoman,48.736,32000,4,1377117,d6084ed25e4af0d7,
|
||||
SFX,9BA833B4:0,AMB_Steel_A1_1,AMB_Steel_A1#1,Ambience,Ambience,,,6.981,32000,4,188172,e3353e1a1a24de74,
|
||||
SFX,9BA833B4:1,AMB_Steel_A1_2,AMB_Steel_A1#2,Ambience,Ambience,,,24.742,32000,4,675277,2bc9fd4e72b321ac,
|
||||
SFX,9BA833B4:2,AMB_Steel_A1_3,AMB_Steel_A1#3,Ambience,Ambience,,,24.742,32000,4,678133,a22748615a708356,
|
||||
SFX,9C8A3A75:0,AMB_RiddlerPuzzle07_1,AMB_RiddlerPuzzle07#1,Ambience,Ambience,,,35.000,32000,4,886167,d0c28304e6b065a8,
|
||||
SFX,9C8A3A75:1,AMB_RiddlerPuzzle07_2,AMB_RiddlerPuzzle07#2,Ambience,Ambience,,,42.000,32000,4,1157261,31b6ec6c6c4d55c1,
|
||||
SFX,9C8A3A7A:0,AMB_RiddlerPuzzle08_1,AMB_RiddlerPuzzle08#1,Ambience,Ambience,,,35.000,32000,4,886167,d0c28304e6b065a8,
|
||||
SFX,9C8A3A7A:1,AMB_RiddlerPuzzle08_2,AMB_RiddlerPuzzle08#2,Ambience,Ambience,,,42.000,32000,4,1157261,31b6ec6c6c4d55c1,
|
||||
SFX,9C903381,NPC_CourtCrowd,NPC_CourtCrowd_1,Ambience,Crowds,,,15.644,32000,4,429416,1ee9589220e2a052,
|
||||
SFX,9DA624C5,AMB_RiddlerChallenge04,AMB_RiddlerChallenge04_1,Ambience,Ambience,,,35.157,32000,4,828243,506937146f3bb8bd,
|
||||
SFX,A9AE706C:0,AMB_Train_1,AMB_Train#1,Ambience,Ambience,,,21.409,32000,4,596226,341a9d16311d8991,
|
||||
SFX,A9AE706C:1,AMB_Train_2,AMB_Train#2,Ambience,Ambience,,,18.693,32000,4,546851,d3b35cb3327cad64,
|
||||
SFX,A9AE706C:2,AMB_Train_3,AMB_Train#3,Ambience,Ambience,,,13.563,32000,4,398654,ef7074176a9b5675,
|
||||
SFX,AA9E3CD8:0,AMB_TheatreB1_1,AMB_TheatreB1#1,Ambience,Ambience,,,36.693,32000,4,914155,fc81b138f8962af4,
|
||||
SFX,AA9E3CD8:1,AMB_TheatreB1_2,AMB_TheatreB1#2,Ambience,Ambience,,,35.072,32000,4,903712,3e175597e848055c,
|
||||
SFX,BA9FCA68,AMB_DemonTrials,AMB_DemonTrials_1,Ambience,Ambience,,,42.411,32000,4,1287623,822e39e9fdb73c7d,
|
||||
SFX,BE349608:0,AMB_BatmanCollapse_1,AMB_BatmanCollapse#1,Ambience,Ambience,,,4.350,32000,5,132523,355a6907a49ba4f7,
|
||||
SFX,BE349608:1,AMB_BatmanCollapse_2,AMB_BatmanCollapse#2,Ambience,Ambience,,,3.838,32000,5,101089,37309cd7d8f681d2,
|
||||
SFX,C4445D54:0,AMB_Underworld_C2_V1_1,AMB_Underworld_C2_V1#1,Ambience,Ambience,,,15.616,32000,4,395281,328d3c9176d28670,
|
||||
SFX,C4445D54:1,AMB_Underworld_C2_V1_2,AMB_Underworld_C2_V1#2,Ambience,Ambience,,,24.064,32000,4,583180,3d41248514f09cbb,
|
||||
SFX,C4445D54:2,AMB_Underworld_C2_V1_3,AMB_Underworld_C2_V1#3,Ambience,Ambience,,,30.207,48000,4,815718,2d95a14358e05838,
|
||||
SFX,D6C65F1E,AMB_PDLC_A1,AMB_PDLC_A1_1,Ambience,Ambience,,,41.643,32000,4,1225237,2386c71bc251bdab,
|
||||
SFX,D6EB8083,AMB_NoraB1,AMB_NoraB1_1,Ambience,Ambience,,,9.461,32000,4,220339,50fa995c758b6ea6,
|
||||
SFX,D7C660F3:0,AMB_PDLC_B1_1,AMB_PDLC_B1#1,Ambience,Ambience,,,29.325,32000,4,766195,7d4546670403542c,
|
||||
SFX,D7C660F3:1,AMB_PDLC_B1_2,AMB_PDLC_B1#2,Ambience,Ambience,,,29.579,32000,4,834813,5785d1e4bb6cde9a,
|
||||
SFX,D7C660F3:2,AMB_PDLC_B1_3,AMB_PDLC_B1#3,Ambience,Ambience,,,32.376,32000,4,865428,a0a0d2b4b83499f7,
|
||||
SFX,DDAFD6E2:0,DDAFD6E2_1,DDAFD6E2#1,Unknown,Unknown,,,1.827,48000,1,10677,a7893dbe34239855,
|
||||
SFX,DDAFD6E2:1,DDAFD6E2_2,DDAFD6E2#2,Unknown,Unknown,,,1.827,48000,1,10677,a7893dbe34239855,
|
||||
SFX,DEA4B422:0,CHR_BM_MadHatterScream_1,CHR_BM_MadHatterScream#1,SFX,Character foley,,Batman,2.531,48000,1,20481,861dcc285c25ad64,
|
||||
SFX,DEA4B422:1,CHR_BM_MadHatterScream_2,CHR_BM_MadHatterScream#2,SFX,Character foley,,Batman,1.465,48000,1,11958,7c711a9df472e6a5,
|
||||
SFX,DEA4B422:2,CHR_BM_MadHatterScream_3,CHR_BM_MadHatterScream#3,SFX,Character foley,,Batman,1.910,48000,1,15540,19a30c9788222076,
|
||||
SFX,DEA4B422:3,CHR_BM_MadHatterScream_4,CHR_BM_MadHatterScream#4,SFX,Character foley,,Batman,1.215,48000,1,9942,1c298f52e501f97e,
|
||||
SFX,DEA4B422:4,CHR_BM_MadHatterScream_5,CHR_BM_MadHatterScream#5,SFX,Character foley,,Batman,1.584,48000,1,13284,538645f2b4c6d26a,
|
||||
SFX,DEA4B422:5,CHR_BM_MadHatterScream_6,CHR_BM_MadHatterScream#6,SFX,Character foley,,Batman,1.577,48000,1,12839,3754080de3dad89a,
|
||||
SFX,E8F980BE/003ACD60,Music_PDLC_003ACD60,,Music,Music (DLC),,,9.227,48000,2,140179,8908f1b982728124,
|
||||
SFX,E8F980BE/004B7F03,Music_PDLC_004B7F03,,Music,Music (DLC),,,6.750,48000,2,101871,044ff907852c49e6,
|
||||
SFX,E8F980BE/0070B665,Music_PDLC_0070B665,,Music,Music (DLC),,,136.744,48000,2,2147137,690c3d62410d9109,
|
||||
SFX,E8F980BE/00768D8F,Music_PDLC_00768D8F,,Music,Music (DLC),,,5.167,48000,2,73843,bed749e799d3f9fc,
|
||||
SFX,E8F980BE/00973366,Music_PDLC_00973366,,Music,Music (DLC),,,7.303,48000,2,102108,d798af7f50edb775,
|
||||
SFX,E8F980BE/00DEE385,Music_PDLC_00DEE385,,Music,Music (DLC),,,136.744,48000,2,1671248,e526b81447e2a0b3,
|
||||
SFX,E8F980BE/00EC1DE2,Music_PDLC_00EC1DE2,,Music,Music (DLC),,,7.782,48000,2,114038,7df7abee227771a6,
|
||||
SFX,E8F980BE/00FF49B2,Music_PDLC_00FF49B2,,Music,Music (DLC),,,15.002,48000,2,246159,d4f4799b844e4972,
|
||||
SFX,E8F980BE/0103E540,Music_PDLC_0103E540,,Music,Music (DLC),,,5.896,48000,2,93093,4c79542c96d2bc82,
|
||||
SFX,E8F980BE/0143D239,Music_PDLC_0143D239,,Music,Music (DLC),,,11.245,48000,2,169362,8e1428544928412d,
|
||||
SFX,E8F980BE/01B5E923,Music_PDLC_01B5E923,,Music,Music (DLC),,,7.066,48000,2,108422,10af8989c0539907,
|
||||
SFX,E8F980BE/01C03EEC,Music_PDLC_01C03EEC,,Music,Music (DLC),,,5.455,48000,2,100514,ff1bed6cc4f9aac3,
|
||||
SFX,E8F980BE/01C4D313,Music_PDLC_01C4D313,,Music,Music (DLC),,,11.667,48000,2,196226,30f7225e239c7468,
|
||||
SFX,E8F980BE/01DF861C,Music_PDLC_01DF861C,,Music,Music (DLC),,,13.752,48000,2,208569,19ce186edc1ab5c0,
|
||||
SFX,E8F980BE/022A88A0,Music_PDLC_022A88A0,,Music,Music (DLC),,,124.043,48000,2,2079213,2ab1c9712fda7b1e,
|
||||
SFX,E8F980BE/023B63DB,Music_PDLC_023B63DB,,Music,Music (DLC),,,143.721,48000,2,1932671,7bc502c5161189e6,
|
||||
SFX,E8F980BE/0299E758,Music_PDLC_0299E758,,Music,Music (DLC),,,120.674,48000,2,2042297,6ed6b11c34f654e7,
|
||||
SFX,E8F980BE/029B900E,Music_PDLC_029B900E,,Music,Music (DLC),,,116.883,48000,2,1881727,34d02b7c101e4ef1,
|
||||
SFX,E8F980BE/02D9C509,Music_PDLC_02D9C509,,Music,Music (DLC),,,124.473,48000,2,1980220,a6c69c660b4d7cbc,
|
||||
SFX,E8F980BE/033EA489,Music_PDLC_033EA489,,Music,Music (DLC),,,118.667,48000,2,1863889,8c691dbb6f681bfd,
|
||||
SFX,E8F980BE/033FCBD2,Music_PDLC_033FCBD2,,Music,Music (DLC),,,4.243,48000,2,61878,08bab5850ea1325b,
|
||||
SFX,E8F980BE/03448FF3,Music_PDLC_03448FF3,,Music,Music (DLC),,,150.000,48000,2,2411615,297784dcc1dc7c92,
|
||||
SFX,E8F980BE/034DAABC,Music_PDLC_034DAABC,,Music,Music (DLC),,,37.091,48000,2,676458,80f6ec4ed1929c82,
|
||||
SFX,E8F980BE/034E5611,Music_PDLC_034E5611,,Music,Music (DLC),,,8.900,48000,2,125034,0d1788a6c0ca8bf3,
|
||||
SFX,E8F980BE/036A3074,Music_PDLC_036A3074,,Music,Music (DLC),,,120.667,48000,2,1874152,3b44666cbaab231c,
|
||||
SFX,E8F980BE/03AE07AB,Music_PDLC_03AE07AB,,Music,Music (DLC),,,121.411,48000,2,1888443,a2413bd7645e6d81,
|
||||
SFX,E8F980BE/03BB3DC6,Music_PDLC_03BB3DC6,,Music,Music (DLC),,,118.667,48000,2,2089926,54828927e669da60,
|
||||
SFX,E8F980BE/03E46F5E,Music_PDLC_03E46F5E,,Music,Music (DLC),,,121.411,48000,2,1946516,8072a4c11cd900b4,
|
||||
SFX,E8F980BE/03E7A461,Music_PDLC_03E7A461,,Music,Music (DLC),,,146.512,48000,2,2031291,5da7b39e1cd1ff0f,
|
||||
SFX,E8F980BE/0482586F,Music_PDLC_0482586F,,Music,Music (DLC),,,13.483,48000,2,193233,33c03a8ec1e8dcb4,
|
||||
SFX,E8F980BE/0484CDE4,Music_PDLC_0484CDE4,,Music,Music (DLC),,,132.600,48000,2,2072532,d093157052677ded,
|
||||
SFX,E8F980BE/050DBEEE,Music_PDLC_050DBEEE,,Music,Music (DLC),,,6.234,48000,2,94644,1bfa0843267bc39f,
|
||||
SFX,E8F980BE/05333001,Music_PDLC_05333001,,Music,Music (DLC),,,120.667,48000,2,1553715,109f7693bcd39fd4,
|
||||
SFX,E8F980BE/0552DAA5,Music_PDLC_0552DAA5,,Music,Music (DLC),,,99.736,48000,2,959593,22139a0192c71238,
|
||||
SFX,E8F980BE/062ABA96,Music_PDLC_062ABA96,,Music,Music (DLC),,,150.000,48000,2,2309451,80a70cbe8f929197,
|
||||
SFX,E8F980BE/064B6476,Music_PDLC_064B6476,,Music,Music (DLC),,,11.056,48000,2,165665,557b7c7ed6489ee0,
|
||||
SFX,E8F980BE/06988828,Music_PDLC_06988828,,Music,Music (DLC),,,10.655,48000,2,153388,b8ff7b3407abcdef,
|
||||
SFX,E8F980BE/06D66515,Music_PDLC_06D66515,,Music,Music (DLC),,,124.160,48000,2,2086304,c878089fa7505965,
|
||||
SFX,E8F980BE/07F084BD,Music_PDLC_07F084BD,,Music,Music (DLC),,,150.000,48000,2,2276849,701309519ea2a457,
|
||||
SFX,E8F980BE/0882D3F9,Music_PDLC_0882D3F9,,Music,Music (DLC),,,174.084,48000,2,2510757,3cef66645f7ac162,
|
||||
SFX,E8F980BE/09899473,Music_PDLC_09899473,,Music,Music (DLC),,,118.667,48000,2,1896684,f0063f284e9c0b92,
|
||||
SFX,E8F980BE/0A398DE7,Music_PDLC_0A398DE7,,Music,Music (DLC),,,37.091,48000,2,591400,b3351a5ac99fadff,
|
||||
SFX,E8F980BE/0AB639E8,Music_PDLC_0AB639E8,,Music,Music (DLC),,,3.870,48000,2,53364,99876e8ee09afd87,
|
||||
SFX,E8F980BE/0AC66BD5,Music_PDLC_0AC66BD5,,Music,Music (DLC),,,4.985,48000,2,78043,3ea7d2f72028d4f7,
|
||||
SFX,E8F980BE/0AE6CF11,Music_PDLC_0AE6CF11,,Music,Music (DLC),,,99.736,48000,2,1496790,5a615508319e4ff0,
|
||||
SFX,E8F980BE/0AEB92B2,Music_PDLC_0AEB92B2,,Music,Music (DLC),,,132.600,48000,2,2070514,5b46ec6590d1fb68,
|
||||
SFX,E8F980BE/0B0A64EC,Music_PDLC_0B0A64EC,,Music,Music (DLC),,,124.043,48000,2,1808300,86e2667fc2cdfaf3,
|
||||
SFX,E8F980BE/0C190AF5,Music_PDLC_0C190AF5,,Music,Music (DLC),,,116.883,48000,2,1873236,2c5b319d5f100636,
|
||||
SFX,E8F980BE/0CF5DF5A,Music_PDLC_0CF5DF5A,,Music,Music (DLC),,,116.883,48000,2,1941276,2463cd547fcdd70a,
|
||||
SFX,E8F980BE/0E0B47BF,Music_PDLC_0E0B47BF,,Music,Music (DLC),,,18.302,48000,2,271552,96ae88c63ce6f0b3,
|
||||
SFX,E8F980BE/0FA4D723,Music_PDLC_0FA4D723,,Music,Music (DLC),,,140.930,48000,2,1786777,e45ac4b09ae534ec,
|
||||
SFX,E8F980BE/1141F701,Music_PDLC_1141F701,,Music,Music (DLC),,,6.159,48000,2,91737,4d51998417d9cd7e,
|
||||
SFX,E8F980BE/114EA5BA,Music_PDLC_114EA5BA,,Music,Music (DLC),,,27.051,48000,2,435229,3c6f42829f0395d7,
|
||||
SFX,E8F980BE/1153EB16,Music_PDLC_1153EB16,,Music,Music (DLC),,,112.514,48000,2,1941069,90c37a7953826bff,
|
||||
SFX,E8F980BE/1306E46D,Music_PDLC_1306E46D,,Music,Music (DLC),,,65.130,48000,2,1058911,9957a92e97ce244f,
|
||||
SFX,E8F980BE/130E1AD0,Music_PDLC_130E1AD0,,Music,Music (DLC),,,9.714,48000,2,138900,16db4fc946de5120,
|
||||
SFX,E8F980BE/136D5FB5,Music_PDLC_136D5FB5,,Music,Music (DLC),,,26.764,48000,2,383894,edbae66d79d37c71,
|
||||
SFX,E8F980BE/13C55A65,Music_PDLC_13C55A65,,Music,Music (DLC),,,32.891,48000,2,515906,916faaffe6c5803d,
|
||||
SFX,E8F980BE/141164AF,Music_PDLC_141164AF,,Music,Music (DLC),,,114.167,48000,2,1722804,789d705f27ac48ff,
|
||||
SFX,E8F980BE/14A4A6BF,Music_PDLC_14A4A6BF,,Music,Music (DLC),,,114.167,48000,2,1925579,d0e77781234a58f8,
|
||||
SFX,E8F980BE/14BE3D5F,Music_PDLC_14BE3D5F,,Music,Music (DLC),,,5.280,48000,2,75630,e17368cc20c49925,
|
||||
SFX,E8F980BE/15561AC7,Music_PDLC_15561AC7,,Music,Music (DLC),,,132.600,48000,2,2266323,964e899a0c6cf057,
|
||||
SFX,E8F980BE/15F943C2,Music_PDLC_15F943C2,,Music,Music (DLC),,,148.310,48000,2,2326714,9002748400da728e,
|
||||
SFX,E8F980BE/19434924,Music_PDLC_19434924,,Music,Music (DLC),,,109.981,48000,2,1635385,1cfde52c13e04959,
|
||||
SFX,E8F980BE/1C80531B,Music_PDLC_1C80531B,,Music,Music (DLC),,,114.167,48000,2,1823209,fa38deac3aa77635,
|
||||
SFX,E8F980BE/1D0EE8DC,Music_PDLC_1D0EE8DC,,Music,Music (DLC),,,119.221,48000,2,1463918,e69ddb4be1b7b84b,
|
||||
SFX,E8F980BE/1F44186D,Music_PDLC_1F44186D,,Music,Music (DLC),,,99.736,48000,2,1514907,c21bde31f78fd34a,
|
||||
SFX,E8F980BE/1F889215,Music_PDLC_1F889215,,Music,Music (DLC),,,120.674,48000,2,1962204,d582d0342bc42104,
|
||||
SFX,E8F980BE/1FFD945F,Music_PDLC_1FFD945F,,Music,Music (DLC),,,18.045,48000,2,259821,4aeb30f7098adbbf,
|
||||
SFX,E8F980BE/24CF1409,Music_PDLC_24CF1409,,Music,Music (DLC),,,26.182,48000,2,456132,e1e55cfe4e98a17b,
|
||||
SFX,E8F980BE/2714A216,Music_PDLC_2714A216,,Music,Music (DLC),,,124.043,48000,2,1981814,289af901611c4269,
|
||||
SFX,E8F980BE/2AA3CEA0,Music_PDLC_2AA3CEA0,,Music,Music (DLC),,,26.182,48000,2,418694,ed57512cccf5c2be,
|
||||
SFX,E8F980BE/2B07A62F,Music_PDLC_2B07A62F,,Music,Music (DLC),,,5.429,48000,2,82642,f7158a1afd44959f,
|
||||
SFX,E8F980BE/3C106E75,Music_PDLC_3C106E75,,Music,Music (DLC),,,6.527,48000,2,95703,7732a64a42bac899,
|
||||
SFX,E8F980BE/3C961A0D,Music_PDLC_3C961A0D,,Music,Music (DLC),,,96.021,48000,2,1405544,5b6c24b48087e2b3,
|
||||
SFX,E9044EB4:0,WND_Wind_Med_1,WND_Wind_Med#1,Ambience,Wind,,,20.000,24000,2,98547,faab78aed8025611,
|
||||
SFX,E9044EB4:1,WND_Wind_Med_2,WND_Wind_Med#2,Ambience,Wind,,,20.000,24000,2,96838,e882b2fe710e4515,
|
||||
SFX,E9044EB4:2,WND_Wind_Med_3,WND_Wind_Med#3,Ambience,Wind,,,20.000,24000,2,95005,fb7e49936a93286f,
|
||||
SFX,E9044EB4:3,WND_Wind_Med_4,WND_Wind_Med#4,Ambience,Wind,,,20.000,24000,2,98328,cafcf14b9664292d,
|
||||
SFX,E9044EB4:4,WND_Wind_Med_5,WND_Wind_Med#5,Ambience,Wind,,,20.000,24000,2,98362,a69db077ae3c396d,
|
||||
SFX,E9044EB4:5,WND_Wind_Med_6,WND_Wind_Med#6,Ambience,Wind,,,20.000,24000,2,98183,2e882fa0c1ccdae3,
|
||||
SFX,E9044EB4:6,WND_Wind_Med_7,WND_Wind_Med#7,Ambience,Wind,,,20.000,24000,2,101052,935c5248c7aa37d8,
|
||||
SFX,E9044EB4:7,WND_Wind_Med_8,WND_Wind_Med#8,Ambience,Wind,,,20.000,24000,2,101958,538515b225fac347,
|
||||
SFX,E9044EB4:8,WND_Wind_Med_9,WND_Wind_Med#9,Ambience,Wind,,,20.000,24000,2,101329,5fba010811aadbb3,
|
||||
SFX,E9044EB4:9,WND_Wind_Med_10,WND_Wind_Med#10,Ambience,Wind,,,20.000,24000,2,102192,65b6a2dfde8c4057,
|
||||
SFX,E9044EB4:10,WND_Wind_Med_11,WND_Wind_Med#11,Ambience,Wind,,,20.000,24000,2,104947,d070102cc6bae5a6,
|
||||
SFX,E9044EB4:11,WND_Wind_Med_12,WND_Wind_Med#12,Ambience,Wind,,,20.000,24000,2,98166,4299280754fe97e5,
|
||||
SFX,E9044EB4:12,WND_Wind_Med_13,WND_Wind_Med#13,Ambience,Wind,,,20.000,24000,2,101738,7d6b5cb8b260d45d,
|
||||
SFX,E9044EB4:13,WND_Wind_Med_14,WND_Wind_Med#14,Ambience,Wind,,,20.000,24000,2,97973,67cbca7dcb8a93bd,
|
||||
SFX,E9044EB4:14,WND_Wind_Med_15,WND_Wind_Med#15,Ambience,Wind,,,20.000,24000,2,95465,fa6f6fbf194be5d9,
|
||||
SFX,E9044EB4:15,WND_Wind_Med_16,WND_Wind_Med#16,Ambience,Wind,,,20.000,24000,2,94809,4a5abcc79b1e0b80,
|
||||
SFX,E9044EB4:16,WND_Wind_Med_17,WND_Wind_Med#17,Ambience,Wind,,,20.000,24000,2,99949,d539e0c11f8f6ab7,
|
||||
SFX,E9044EB4:17,WND_Wind_Med_18,WND_Wind_Med#18,Ambience,Wind,,,20.000,24000,2,100418,6d1eaf75c4de53ac,
|
||||
SFX,E9044EB4:18,WND_Wind_Med_19,WND_Wind_Med#19,Ambience,Wind,,,20.000,24000,2,99048,2dc97369646e6958,
|
||||
SFX,E9044EB4:19,WND_Wind_Med_20,WND_Wind_Med#20,Ambience,Wind,,,20.000,24000,2,97280,36dcf4ccdebdf010,
|
||||
SFX,E9044EB4:20,WND_Wind_Med_21,WND_Wind_Med#21,Ambience,Wind,,,20.000,24000,2,101748,e8d6d2edc55c2cf2,
|
||||
SFX,E9044EB4:21,WND_Wind_Med_22,WND_Wind_Med#22,Ambience,Wind,,,20.000,24000,2,98163,3fa3cb70185cab36,
|
||||
SFX,E9044EB4:22,WND_Wind_Med_23,WND_Wind_Med#23,Ambience,Wind,,,20.000,24000,2,98468,9abafc3360d4dde0,
|
||||
SFX,EBF95C27,AMB_CWB1,AMB_CWB1_1,Ambience,Ambience,,,16.717,32000,4,482955,b5ce51f30e01cc11,
|
||||
SFX,EDF036D6/003CB311,Music_003CB311,,Music,Music,,,29.760,48000,2,451510,eb4cdddd0ebce65d,
|
||||
SFX,EDF036D6/004B7F03,Music_004B7F03,,Music,Music,,,6.750,48000,2,101871,044ff907852c49e6,
|
||||
SFX,EDF036D6/006AC1CF,Music_006AC1CF,,Music,Music,,,8.000,48000,2,121670,ba0f4a31a96bbf0a,
|
||||
SFX,EDF036D6/00768D8F,Music_00768D8F,,Music,Music,,,5.167,48000,2,73843,bed749e799d3f9fc,
|
||||
SFX,EDF036D6/0077966A,Music_0077966A,,Music,Music,,,139.862,48000,2,2240069,0aa0ab711de88e99,
|
||||
SFX,EDF036D6/0091757B,Music_0091757B,,Music,Music,,,136.358,48000,2,2079414,036ba4f5985870f5,
|
||||
SFX,EDF036D6/0094ECE0,Music_0094ECE0,,Music,Music,,,28.000,48000,2,427330,c809d53c9ac3a4ff,
|
||||
SFX,EDF036D6/00973366,Music_00973366,,Music,Music,,,7.303,48000,2,102108,d798af7f50edb775,
|
||||
SFX,EDF036D6/00CF19F6,Music_00CF19F6,,Music,Music,,,36.651,48000,2,652015,21fd45e0ab2f46a3,
|
||||
SFX,EDF036D6/00EC1DE2,Music_00EC1DE2,,Music,Music,,,7.782,48000,2,114038,7df7abee227771a6,
|
||||
SFX,EDF036D6/00FA6232,Music_00FA6232,,Music,Music,,,136.744,48000,2,2147137,690c3d62410d9109,
|
||||
SFX,EDF036D6/00FF49B2,Music_00FF49B2,,Music,Music,,,15.002,48000,2,246159,d4f4799b844e4972,
|
||||
SFX,EDF036D6/0103E540,Music_0103E540,,Music,Music,,,5.896,48000,2,93093,4c79542c96d2bc82,
|
||||
SFX,EDF036D6/0143D239,Music_0143D239,,Music,Music,,,11.245,48000,2,169362,8e1428544928412d,
|
||||
SFX,EDF036D6/01714272,Music_01714272,,Music,Music,,,134.234,48000,2,1903999,ac187b1ed9b3e297,
|
||||
SFX,EDF036D6/0187CAC4,Music_0187CAC4,,Music,Music,,,5.969,48000,2,85367,fcca428404e1ee90,
|
||||
SFX,EDF036D6/018F4EE2,Music_018F4EE2,,Music,Music,,,15.360,48000,2,267100,ff8bebf954391f4f,
|
||||
SFX,EDF036D6/01916433,Music_01916433,,Music,Music,,,18.100,48000,2,262068,cdee9195621576d5,
|
||||
SFX,EDF036D6/01B5E923,Music_01B5E923,,Music,Music,,,7.066,48000,2,108422,10af8989c0539907,
|
||||
SFX,EDF036D6/01C03EEC,Music_01C03EEC,,Music,Music,,,5.455,48000,2,100514,ff1bed6cc4f9aac3,
|
||||
SFX,EDF036D6/01C4D313,Music_01C4D313,,Music,Music,,,11.667,48000,2,196226,30f7225e239c7468,
|
||||
SFX,EDF036D6/01C73384,Music_01C73384,,Music,Music,,,21.404,48000,2,307229,4b1412e1904010f6,
|
||||
SFX,EDF036D6/01DF861C,Music_01DF861C,,Music,Music,,,13.752,48000,2,208569,19ce186edc1ab5c0,
|
||||
SFX,EDF036D6/022A88A0,Music_022A88A0,,Music,Music,,,124.043,48000,2,2079213,2ab1c9712fda7b1e,
|
||||
SFX,EDF036D6/0236F058,Music_0236F058,,Music,Music,,,10.339,48000,2,157180,f69af5f5fa7089cc,
|
||||
SFX,EDF036D6/023B63DB,Music_023B63DB,,Music,Music,,,143.721,48000,2,1932671,7bc502c5161189e6,
|
||||
SFX,EDF036D6/0299E758,Music_0299E758,,Music,Music,,,120.674,48000,2,2042297,6ed6b11c34f654e7,
|
||||
SFX,EDF036D6/02BD77FA,Music_02BD77FA,,Music,Music,,,4.177,48000,2,67717,eb9870b55a8edf72,
|
||||
SFX,EDF036D6/030666A2,Music_030666A2,,Music,Music,,,2.118,48000,2,30627,9c49ed214407c3f7,
|
||||
SFX,EDF036D6/030EAAF4,Music_030EAAF4,,Music,Music,,,27.001,48000,2,417402,a8aae159b0b70e0f,
|
||||
SFX,EDF036D6/033EA489,Music_033EA489,,Music,Music,,,118.667,48000,2,1863889,8c691dbb6f681bfd,
|
||||
SFX,EDF036D6/033FCBD2,Music_033FCBD2,,Music,Music,,,4.243,48000,2,61878,08bab5850ea1325b,
|
||||
SFX,EDF036D6/03448FF3,Music_03448FF3,,Music,Music,,,150.000,48000,2,2411615,297784dcc1dc7c92,
|
||||
SFX,EDF036D6/036A3074,Music_036A3074,,Music,Music,,,120.667,48000,2,1874152,3b44666cbaab231c,
|
||||
SFX,EDF036D6/03A77EDB,Music_03A77EDB,,Music,Music,,,5.056,48000,2,74536,0358b137e007ef69,
|
||||
SFX,EDF036D6/03AE07AB,Music_03AE07AB,,Music,Music,,,121.411,48000,2,1888443,a2413bd7645e6d81,
|
||||
SFX,EDF036D6/03BB3DC6,Music_03BB3DC6,,Music,Music,,,118.667,48000,2,2089926,54828927e669da60,
|
||||
SFX,EDF036D6/03CF5072,Music_03CF5072,,Music,Music,,,5.355,48000,2,77471,8c8e11b1b6730eee,
|
||||
SFX,EDF036D6/03E46F5E,Music_03E46F5E,,Music,Music,,,121.411,48000,2,1946516,8072a4c11cd900b4,
|
||||
SFX,EDF036D6/03E7A461,Music_03E7A461,,Music,Music,,,146.512,48000,2,2031291,5da7b39e1cd1ff0f,
|
||||
SFX,EDF036D6/03EF5052,Music_03EF5052,,Music,Music,,,10.933,48000,2,165614,0a81b7aed4e19666,
|
||||
SFX,EDF036D6/03F43679,Music_03F43679,,Music,Music,,,65.479,48000,2,910455,136a1d2be974aca4,
|
||||
SFX,EDF036D6/03F81E23,Music_03F81E23,,Music,Music,,,12.827,48000,2,220428,346c196b48cc25f1,
|
||||
SFX,EDF036D6/041B0207,Music_041B0207,,Music,Music,,,7.273,48000,2,115780,3b54e829d62e2cdd,
|
||||
SFX,EDF036D6/041B3ED1,Music_041B3ED1,,Music,Music,,,136.571,48000,2,2156010,55794b221524244e,
|
||||
SFX,EDF036D6/045740E7,Music_045740E7,,Music,Music,,,124.473,48000,2,1980220,a6c69c660b4d7cbc,
|
||||
SFX,EDF036D6/04626D74,Music_04626D74,,Music,Music,,,33.231,48000,2,497870,7c8229f48538d37f,
|
||||
SFX,EDF036D6/046D8256,Music_046D8256,,Music,Music,,,116.883,48000,2,1881727,34d02b7c101e4ef1,
|
||||
SFX,EDF036D6/0482586F,Music_0482586F,,Music,Music,,,13.483,48000,2,193233,33c03a8ec1e8dcb4,
|
||||
SFX,EDF036D6/0484CDE4,Music_0484CDE4,,Music,Music,,,132.600,48000,2,2072532,d093157052677ded,
|
||||
SFX,EDF036D6/04A296D5,Music_04A296D5,,Music,Music,,,139.862,48000,2,2050965,ed4d836843d773e2,
|
||||
SFX,EDF036D6/04C378D7,Music_04C378D7,,Music,Music,,,8.048,48000,2,121070,ddb4f685b987aa77,
|
||||
SFX,EDF036D6/04DBE3AB,Music_04DBE3AB,,Music,Music,,,59.521,48000,2,818632,1fc309fc1ad2e944,
|
||||
SFX,EDF036D6/0518B1B7,Music_0518B1B7,,Music,Music,,,11.427,48000,2,169289,2c178bbdbf1925b9,
|
||||
SFX,EDF036D6/05333001,Music_05333001,,Music,Music,,,120.667,48000,2,1553715,109f7693bcd39fd4,
|
||||
SFX,EDF036D6/05470280,Music_05470280,,Music,Music,,,128.764,48000,2,1746550,b33dd74c1bb0a7f1,
|
||||
SFX,EDF036D6/05513772,Music_05513772,,Music,Music,,,120.674,48000,2,1815939,ef2aa623f394818d,
|
||||
SFX,EDF036D6/0552DAA5,Music_0552DAA5,,Music,Music,,,99.736,48000,2,959593,22139a0192c71238,
|
||||
SFX,EDF036D6/058231ED,Music_058231ED,,Music,Music,,,68.211,48000,2,1138245,86df0acdfdfc1df0,
|
||||
SFX,EDF036D6/0587577E,Music_0587577E,,Music,Music,,,139.862,48000,2,2198296,560ce46975841a7e,
|
||||
SFX,EDF036D6/05C8D32A,Music_05C8D32A,,Music,Music,,,26.526,48000,2,343088,73bef0da1727bba3,
|
||||
SFX,EDF036D6/060E415F,Music_060E415F,,Music,Music,,,11.654,48000,2,161817,8f0c7e25073d4374,
|
||||
SFX,EDF036D6/062ABA96,Music_062ABA96,,Music,Music,,,150.000,48000,2,2309451,80a70cbe8f929197,
|
||||
SFX,EDF036D6/064B6476,Music_064B6476,,Music,Music,,,11.056,48000,2,165665,557b7c7ed6489ee0,
|
||||
SFX,EDF036D6/06503115,Music_06503115,,Music,Music,,,57.931,48000,2,1145563,979b62622e8d74dd,
|
||||
SFX,EDF036D6/067C3EF1,Music_067C3EF1,,Music,Music,,,10.288,48000,2,144367,4b61f96005d0861e,
|
||||
SFX,EDF036D6/06963560,Music_06963560,,Music,Music,,,10.541,48000,2,169361,4684815a1a026ef9,
|
||||
SFX,EDF036D6/06988828,Music_06988828,,Music,Music,,,10.655,48000,2,153388,b8ff7b3407abcdef,
|
||||
SFX,EDF036D6/06AD24B7,Music_06AD24B7,,Music,Music,,,13.664,48000,2,216979,2e9d08baf192bc27,
|
||||
SFX,EDF036D6/06D66515,Music_06D66515,,Music,Music,,,124.160,48000,2,2086304,c878089fa7505965,
|
||||
SFX,EDF036D6/06E7FD0F,Music_06E7FD0F,,Music,Music,,,220.337,48000,2,3283587,97a25b3b1e86da70,
|
||||
SFX,EDF036D6/06F1B02B,Music_06F1B02B,,Music,Music,,,10.909,48000,2,175394,906d1ab34e0676c5,
|
||||
SFX,EDF036D6/0748B510,Music_0748B510,,Music,Music,,,4.637,48000,2,65408,c19c38f670f623da,
|
||||
SFX,EDF036D6/0792E524,Music_0792E524,,Music,Music,,,63.140,48000,2,953656,401d370d42bcacf2,
|
||||
SFX,EDF036D6/0793DE7C,Music_0793DE7C,,Music,Music,,,34.415,48000,2,490514,da62323a54973ef5,
|
||||
SFX,EDF036D6/07C1C384,Music_07C1C384,,Music,Music,,,7.579,48000,2,127218,624b4083a829d00a,
|
||||
SFX,EDF036D6/07D74727,Music_07D74727,,Music,Music,,,5.455,48000,2,83321,e7586180df9e15d2,
|
||||
SFX,EDF036D6/07D7BB90,Music_07D7BB90,,Music,Music,,,128.764,48000,2,2069659,b28e2b67e237e1d4,
|
||||
SFX,EDF036D6/07ED9BED,Music_07ED9BED,,Music,Music,,,5.000,48000,2,83513,3ac88d66cc7e2af4,
|
||||
SFX,EDF036D6/07F084BD,Music_07F084BD,,Music,Music,,,150.000,48000,2,2276849,701309519ea2a457,
|
||||
SFX,EDF036D6/08028AF6,Music_08028AF6,,Music,Music,,,27.032,48000,2,447037,3ea40013c34c5bfb,
|
||||
SFX,EDF036D6/08350005,Music_08350005,,Music,Music,,,10.163,48000,2,163179,53b8f892cec18939,
|
||||
SFX,EDF036D6/0879D4B8,Music_0879D4B8,,Music,Music,,,31.054,48000,2,432075,b0eae4067e80b8a4,
|
||||
SFX,EDF036D6/08B01ABF,Music_08B01ABF,,Music,Music,,,5.920,48000,2,104192,fe272d172651fd13,
|
||||
SFX,EDF036D6/08B289F1,Music_08B289F1,,Music,Music,,,59.951,48000,2,968149,3c93984f24d71dcf,
|
||||
SFX,EDF036D6/08DEC601,Music_08DEC601,,Music,Music,,,7.984,48000,2,118158,4dc163d490d6b082,
|
||||
SFX,EDF036D6/08FDF59F,Music_08FDF59F,,Music,Music,,,4.027,48000,2,59853,d642287171dac178,
|
||||
SFX,EDF036D6/08FEB583,Music_08FEB583,,Music,Music,,,111.019,48000,2,1957859,e51988fee897cf9a,
|
||||
SFX,EDF036D6/09268C8B,Music_09268C8B,,Music,Music,,,51.796,48000,2,792757,5ed884a66cbd99f4,
|
||||
SFX,EDF036D6/09475E4C,Music_09475E4C,,Music,Music,,,59.200,48000,3,1046008,744aa70e18aeb5bd,
|
||||
SFX,EDF036D6/0963C63E,Music_0963C63E,,Music,Music,,,136.744,48000,2,1671248,e526b81447e2a0b3,
|
||||
SFX,EDF036D6/09662D97,Music_09662D97,,Music,Music,,,51.393,48000,2,705497,a886aa3314db3230,
|
||||
SFX,EDF036D6/0966308B,Music_0966308B,,Music,Music,,,145.655,48000,2,2252265,c226fa9ab4947989,
|
||||
SFX,EDF036D6/09899473,Music_09899473,,Music,Music,,,118.667,48000,2,1896684,f0063f284e9c0b92,
|
||||
SFX,EDF036D6/09B90B1F,Music_09B90B1F,,Music,Music,,,12.957,48000,2,191397,4f07c23037242a86,
|
||||
SFX,EDF036D6/09BCB885,Music_09BCB885,,Music,Music,,,27.183,48000,2,355145,452006f7d55b7542,
|
||||
SFX,EDF036D6/09DF28BC,Music_09DF28BC,,Music,Music,,,8.900,48000,2,125034,0d1788a6c0ca8bf3,
|
||||
SFX,EDF036D6/0A270868,Music_0A270868,,Music,Music,,,56.114,48000,2,829545,9b3d70dfd1d0bf7d,
|
||||
SFX,EDF036D6/0A3391C4,Music_0A3391C4,,Music,Music,,,56.034,48000,2,862686,f56d0c0a35170987,
|
||||
SFX,EDF036D6/0A35D517,Music_0A35D517,,Music,Music,,,49.232,48000,2,695149,6156c833d17fa9d5,
|
||||
SFX,EDF036D6/0A398DE7,Music_0A398DE7,,Music,Music,,,37.091,48000,2,591407,ac67b2eaff5065d5,
|
||||
SFX,EDF036D6/0A45BC45,Music_0A45BC45,,Music,Music,,,7.026,48000,2,110650,a81eeb8497dc6087,
|
||||
SFX,EDF036D6/0A514BC5,Music_0A514BC5,,Music,Music,,,5.031,48000,2,69166,27a2d8a8524eabdc,
|
||||
SFX,EDF036D6/0A6A4BEA,Music_0A6A4BEA,,Music,Music,,,120.674,48000,2,1998826,63ec01f5a15e3625,
|
||||
SFX,EDF036D6/0AB639E8,Music_0AB639E8,,Music,Music,,,3.870,48000,2,53364,99876e8ee09afd87,
|
||||
SFX,EDF036D6/0AE2EB37,Music_0AE2EB37,,Music,Music,,,9.227,48000,2,140179,8908f1b982728124,
|
||||
SFX,EDF036D6/0AE6CF11,Music_0AE6CF11,,Music,Music,,,99.736,48000,2,1496790,5a615508319e4ff0,
|
||||
SFX,EDF036D6/0AE6E55A,Music_0AE6E55A,,Music,Music,,,5.732,48000,2,78011,f1ce12bc172ee924,
|
||||
SFX,EDF036D6/0B074B6F,Music_0B074B6F,,Music,Music,,,15.401,48000,2,236110,164b12aebce490d7,
|
||||
SFX,EDF036D6/0B0A64EC,Music_0B0A64EC,,Music,Music,,,124.043,48000,2,1808300,86e2667fc2cdfaf3,
|
||||
SFX,EDF036D6/0B591BEA,Music_0B591BEA,,Music,Music,,,9.600,48000,2,137803,8742c5db432792e2,
|
||||
SFX,EDF036D6/0B8D3FA7,Music_0B8D3FA7,,Music,Music,,,37.091,48000,2,676472,c80ab19d29f54db2,
|
||||
SFX,EDF036D6/0B934098,Music_0B934098,,Music,Music,,,130.386,48000,6,5433905,542486c69ab21c73,
|
||||
SFX,EDF036D6/0BAFE185,Music_0BAFE185,,Music,Music,,,120.000,48000,2,1870515,af2b9390c746d55a,
|
||||
SFX,EDF036D6/0BCB4DFB,Music_0BCB4DFB,,Music,Music,,,11.194,48000,2,157866,1893d0875bc0324c,
|
||||
SFX,EDF036D6/0BD2F54C,Music_0BD2F54C,,Music,Music,,,4.000,48000,2,62058,c1364139c5ab6796,
|
||||
SFX,EDF036D6/0BE54B8B,Music_0BE54B8B,,Music,Music,,,82.587,48000,2,1154038,b91f2a7731e80d93,
|
||||
SFX,EDF036D6/0C190AF5,Music_0C190AF5,,Music,Music,,,116.883,48000,2,1873236,2c5b319d5f100636,
|
||||
SFX,EDF036D6/0C786C68,Music_0C786C68,,Music,Music,,,25.455,48000,2,374798,9f32e428d0415a75,
|
||||
SFX,EDF036D6/0CF5DF5A,Music_0CF5DF5A,,Music,Music,,,116.883,48000,2,1941276,2463cd547fcdd70a,
|
||||
SFX,EDF036D6/0D080AF2,Music_0D080AF2,,Music,Music,,,24.841,48000,2,227956,3f0d0f61677ac580,
|
||||
SFX,EDF036D6/0D116946,Music_0D116946,,Music,Music,,,136.571,48000,2,2261132,683f0bf6dc70b451,
|
||||
SFX,EDF036D6/0D1D673E,Music_0D1D673E,,Music,Music,,,12.809,48000,2,217995,78bab68bfcaa5d43,
|
||||
SFX,EDF036D6/0D395255,Music_0D395255,,Music,Music,,,114.880,48000,2,1771509,e2cdde27731d21d9,
|
||||
SFX,EDF036D6/0D5E8A4E,Music_0D5E8A4E,,Music,Music,,,34.172,48000,6,620536,63c45a778bf37bf5,
|
||||
SFX,EDF036D6/0D9F4730,Music_0D9F4730,,Music,Music,,,114.884,48000,2,1706066,42cfd6f97989241c,
|
||||
SFX,EDF036D6/0DA55A9A,Music_0DA55A9A,,Music,Music,,,67.830,24000,2,349704,a4a8b9249bdc87ef,
|
||||
SFX,EDF036D6/0DCDB7EA,Music_0DCDB7EA,,Music,Music,,,61.519,48000,6,1138993,4f2c30017b90638f,
|
||||
SFX,EDF036D6/0DCF4A58,Music_0DCF4A58,,Music,Music,,,25.703,48000,2,365874,3cbd811d75dec15e,
|
||||
SFX,EDF036D6/0DD4CB01,Music_0DD4CB01,,Music,Music,,,9.230,48000,2,132559,3b88efd559836afd,
|
||||
SFX,EDF036D6/0DD67C32,Music_0DD67C32,,Music,Music,,,132.600,48000,2,2070514,5b46ec6590d1fb68,
|
||||
SFX,EDF036D6/0DD82CDF,Music_0DD82CDF,,Music,Music,,,66.801,48000,2,973802,5632edb072828857,
|
||||
SFX,EDF036D6/0E0B47BF,Music_0E0B47BF,,Music,Music,,,18.302,48000,2,271552,96ae88c63ce6f0b3,
|
||||
SFX,EDF036D6/0E0D5646,Music_0E0D5646,,Music,Music,,,10.909,48000,2,179745,740b7105d6b42ff6,
|
||||
SFX,EDF036D6/0E0F52CB,Music_0E0F52CB,,Music,Music,,,7.579,48000,2,102397,73ef4f5e53d2b4b1,
|
||||
SFX,EDF036D6/0E6EA120,Music_0E6EA120,,Music,Music,,,4.985,48000,2,78043,3ea7d2f72028d4f7,
|
||||
SFX,EDF036D6/0E8284B0,Music_0E8284B0,,Music,Music,,,13.633,48000,2,200731,0fb0d5eaea927482,
|
||||
SFX,EDF036D6/0EC73393,Music_0EC73393,,Music,Music,,,56.689,48000,2,843627,8bce3f68a65ae479,
|
||||
SFX,EDF036D6/0F1BB771,Music_0F1BB771,,Music,Music,,,5.357,48000,2,79456,98a6143e52c6fe9c,
|
||||
SFX,EDF036D6/0F2E5BE7,Music_0F2E5BE7,,Music,Music,,,115.955,48000,2,1780926,7f34763c2bac1cbc,
|
||||
SFX,EDF036D6/0F927604,Music_0F927604,,Music,Music,,,4.100,48000,2,60427,341074b2bcd2cc3e,
|
||||
SFX,EDF036D6/0FA4D723,Music_0FA4D723,,Music,Music,,,140.930,48000,2,1786777,e45ac4b09ae534ec,
|
||||
SFX,EDF036D6/100725CE,Music_100725CE,,Music,Music,,,138.805,48000,2,1988903,aede3b9c498119bd,
|
||||
SFX,EDF036D6/103350C6,Music_103350C6,,Music,Music,,,10.706,48000,2,163488,6cd9394131897d8f,
|
||||
SFX,EDF036D6/1045B2D0,Music_1045B2D0,,Music,Music,,,26.318,48000,2,439396,49ad8f1567a94ab2,
|
||||
SFX,EDF036D6/107651F0,Music_107651F0,,Music,Music,,,11.031,48000,2,154397,dc018d1059ac9132,
|
||||
SFX,EDF036D6/10CEC694,Music_10CEC694,,Music,Music,,,21.783,48000,2,337478,838175d3a11af025,
|
||||
SFX,EDF036D6/10D95181,Music_10D95181,,Music,Music,,,62.161,48000,2,943143,4ec6adc0ff80df6f,
|
||||
SFX,EDF036D6/10DD7E39,Music_10DD7E39,,Music,Music,,,41.038,48000,2,574465,c073bbdbf9162a17,
|
||||
SFX,EDF036D6/10EF999B,Music_10EF999B,,Music,Music,,,7.836,48000,2,54377,30f28b938ba61758,
|
||||
SFX,EDF036D6/11011607,Music_11011607,,Music,Music,,,64.928,48000,2,1008393,2f84bf32973e615f,
|
||||
SFX,EDF036D6/11213CD9,Music_11213CD9,,Music,Music,,,5.472,48000,2,74798,5636cd7483facacc,
|
||||
SFX,EDF036D6/1141F701,Music_1141F701,,Music,Music,,,6.159,48000,2,91737,4d51998417d9cd7e,
|
||||
SFX,EDF036D6/114C2C42,Music_114C2C42,,Music,Music,,,57.931,48000,2,1210901,d7ecc9ca26aca3d3,
|
||||
SFX,EDF036D6/114EA5BA,Music_114EA5BA,,Music,Music,,,27.051,48000,2,435215,cb7c21d127a57bda,
|
||||
SFX,EDF036D6/114F6595,Music_114F6595,,Music,Music,,,137.553,48000,2,2138964,6ea746351dcabf9c,
|
||||
SFX,EDF036D6/11749351,Music_11749351,,Music,Music,,,110.909,48000,2,1700949,cc8c7f8fe193a36d,
|
||||
SFX,EDF036D6/1184538D,Music_1184538D,,Music,Music,,,57.931,48000,2,836829,7ae3a034a1a14842,
|
||||
SFX,EDF036D6/11B3505F,Music_11B3505F,,Music,Music,,,119.040,48000,2,1804042,7ea5b6c29fc28a96,
|
||||
SFX,EDF036D6/11D06D0C,Music_11D06D0C,,Music,Music,,,64.669,48000,2,960537,28e584eeced2b6ce,
|
||||
SFX,EDF036D6/11D477D1,Music_11D477D1,,Music,Music,,,69.629,48000,2,976260,12a1237d1346aa35,
|
||||
SFX,EDF036D6/1231904E,Music_1231904E,,Music,Music,,,5.472,48000,2,79207,828e7962c313978d,
|
||||
SFX,EDF036D6/12CB7EA2,Music_12CB7EA2,,Music,Music,,,21.063,48000,2,306437,d13a3193ca5a8541,
|
||||
SFX,EDF036D6/12DFC5A3,Music_12DFC5A3,,Music,Music,,,120.674,48000,2,1905166,3722d236172a35dd,
|
||||
SFX,EDF036D6/12F0D318,Music_12F0D318,,Music,Music,,,25.576,48000,2,460855,970213287c121e61,
|
||||
SFX,EDF036D6/130425A1,Music_130425A1,,Music,Music,,,4.638,48000,2,69039,39d636a6c20cb4a9,
|
||||
SFX,EDF036D6/1304DD30,Music_1304DD30,,Music,Music,,,6.124,48000,2,79539,485a9a0e2a46c58a,
|
||||
SFX,EDF036D6/1306E46D,Music_1306E46D,,Music,Music,,,65.130,48000,2,1058911,9957a92e97ce244f,
|
||||
SFX,EDF036D6/130E1AD0,Music_130E1AD0,,Music,Music,,,9.714,48000,2,138900,16db4fc946de5120,
|
||||
SFX,EDF036D6/134A4A91,Music_134A4A91,,Music,Music,,,12.488,48000,2,176307,69bfef90f2dacf24,
|
||||
SFX,EDF036D6/136D5FB5,Music_136D5FB5,,Music,Music,,,26.764,48000,2,383894,edbae66d79d37c71,
|
||||
SFX,EDF036D6/1388391B,Music_1388391B,,Music,Music,,,92.559,48000,2,1237011,7d966e508d118f35,
|
||||
SFX,EDF036D6/138BDCCF,Music_138BDCCF,,Music,Music,,,7.680,48000,2,109178,65e4ba1a2297baa1,
|
||||
SFX,EDF036D6/139E54D8,Music_139E54D8,,Music,Music,,,7.321,48000,2,119246,672ff00e9ef04c26,
|
||||
SFX,EDF036D6/13A0FCF1,Music_13A0FCF1,,Music,Music,,,33.822,48000,6,620811,113344a096e024c9,
|
||||
SFX,EDF036D6/13D03B5C,Music_13D03B5C,,Music,Music,,,124.148,48000,2,1917760,243ae8dcce359a64,
|
||||
SFX,EDF036D6/13FC57BF,Music_13FC57BF,,Music,Music,,,11.864,48000,2,175369,b9851e6d3b65b196,
|
||||
SFX,EDF036D6/141164AF,Music_141164AF,,Music,Music,,,114.167,48000,2,1722804,789d705f27ac48ff,
|
||||
SFX,EDF036D6/1417F135,Music_1417F135,,Music,Music,,,18.416,48000,2,252360,502fb3a6747f1edc,
|
||||
SFX,EDF036D6/142BD546,Music_142BD546,,Music,Music,,,33.600,48000,2,469861,72b38f1a2d26dd8f,
|
||||
SFX,EDF036D6/145794F0,Music_145794F0,,Music,Music,,,4.667,48000,2,65468,3b6e4b9c36e9809b,
|
||||
SFX,EDF036D6/149379DA,Music_149379DA,,Music,Music,,,11.324,48000,2,155217,42e0d077e4514dce,
|
||||
SFX,EDF036D6/14949A24,Music_14949A24,,Music,Music,,,68.211,48000,2,1014148,c63a08eb40b2c73c,
|
||||
SFX,EDF036D6/14BE3D5F,Music_14BE3D5F,,Music,Music,,,5.280,48000,2,75630,e17368cc20c49925,
|
||||
SFX,EDF036D6/14C7314B,Music_14C7314B,,Music,Music,,,7.083,48000,2,110811,c4cc9738a29f1c64,
|
||||
SFX,EDF036D6/14E1DF85,Music_14E1DF85,,Music,Music,,,6.998,48000,2,102881,6aa334d345da108a,
|
||||
SFX,EDF036D6/15166641,Music_15166641,,Music,Music,,,9.931,48000,2,120665,66da27b2a11d71bb,
|
||||
SFX,EDF036D6/1549DDB2,Music_1549DDB2,,Music,Music,,,37.091,48000,2,595527,01355f355c0b2853,
|
||||
SFX,EDF036D6/15561AC7,Music_15561AC7,,Music,Music,,,132.600,48000,2,2266323,964e899a0c6cf057,
|
||||
SFX,EDF036D6/15F24B15,Music_15F24B15,,Music,Music,,,7.788,48000,2,122403,4017b5d94a68cd74,
|
||||
SFX,EDF036D6/163C8641,Music_163C8641,,Music,Music,,,30.583,48000,2,418441,602775842cc2eea1,
|
||||
SFX,EDF036D6/166847B7,Music_166847B7,,Music,Music,,,11.272,48000,2,156467,86814d3b24ff9b7e,
|
||||
SFX,EDF036D6/168E0EDC,Music_168E0EDC,,Music,Music,,,64.527,48000,2,899368,3cf382b0b081a7ba,
|
||||
SFX,EDF036D6/16AEE4FD,Music_16AEE4FD,,Music,Music,,,4.739,48000,2,75711,5e466c8bf9a4d172,
|
||||
SFX,EDF036D6/16C59E4D,Music_16C59E4D,,Music,Music,,,27.006,48000,2,479283,4171a30575d9123e,
|
||||
SFX,EDF036D6/17A65BC8,Music_17A65BC8,,Music,Music,,,51.855,48000,2,735688,93e09affa01d21b5,
|
||||
SFX,EDF036D6/17B8E786,Music_17B8E786,,Music,Music,,,6.234,48000,2,94644,1bfa0843267bc39f,
|
||||
SFX,EDF036D6/18177B73,Music_18177B73,,Music,Music,,,49.880,48000,2,737499,888ab9710ee6bc1d,
|
||||
SFX,EDF036D6/187BD87F,Music_187BD87F,,Music,Music,,,27.580,48000,2,407557,a0d79446b1b33012,
|
||||
SFX,EDF036D6/18CE2848,Music_18CE2848,,Music,Music,,,16.726,48000,2,188870,60ab2664a3f64afd,
|
||||
SFX,EDF036D6/190E481E,Music_190E481E,,Music,Music,,,56.486,48000,2,804642,5d6b1680a58b085a,
|
||||
SFX,EDF036D6/1951CABD,Music_1951CABD,,Music,Music,,,20.105,48000,2,331402,4530a35f0898041a,
|
||||
SFX,EDF036D6/1A2A956A,Music_1A2A956A,,Music,Music,,,120.674,48000,2,1773755,c5b6ce6781144d37,
|
||||
SFX,EDF036D6/1B55A21D,Music_1B55A21D,,Music,Music,,,148.310,48000,2,2326714,9002748400da728e,
|
||||
SFX,EDF036D6/1B995440,Music_1B995440,,Music,Music,,,82.719,48000,2,1230786,4eea50935d8861bb,
|
||||
SFX,EDF036D6/1BB0C4AD,Music_1BB0C4AD,,Music,Music,,,136.568,48000,2,1695285,537e4507c59cb4dd,
|
||||
SFX,EDF036D6/1C0025EC,Music_1C0025EC,,Music,Music,,,55.204,48000,2,851331,ae51ad07eadff2a0,
|
||||
SFX,EDF036D6/1C0067DB,Music_1C0067DB,,Music,Music,,,174.084,48000,2,2510757,3cef66645f7ac162,
|
||||
SFX,EDF036D6/1C070DC7,Music_1C070DC7,,Music,Music,,,15.158,48000,2,243328,7c2eb9df93a9dba7,
|
||||
SFX,EDF036D6/1D5BDC25,Music_1D5BDC25,,Music,Music,,,30.005,48000,2,499008,d8217f8ccde1a7c6,
|
||||
SFX,EDF036D6/1D9BCFEA,Music_1D9BCFEA,,Music,Music,,,27.051,48000,2,346917,4f5341cdf41e3dfe,
|
||||
SFX,EDF036D6/1D9C5132,Music_1D9C5132,,Music,Music,,,18.876,48000,2,334388,c3d9f7b58e388452,
|
||||
SFX,EDF036D6/1DBF5552,Music_1DBF5552,,Music,Music,,,124.212,48000,2,1883930,905e1ec95b5de2c1,
|
||||
SFX,EDF036D6/1DC5D065,Music_1DC5D065,,Music,Music,,,49.865,48000,6,909285,0c96d8d8a0e119c7,
|
||||
SFX,EDF036D6/1DE02287,Music_1DE02287,,Music,Music,,,128.764,48000,2,1957556,b3e0906c43c188af,
|
||||
SFX,EDF036D6/1E326B83,Music_1E326B83,,Music,Music,,,13.653,48000,2,215358,a5334288e71969f0,
|
||||
SFX,EDF036D6/1E7F8C2D,Music_1E7F8C2D,,Music,Music,,,10.645,48000,2,163632,61797286fd511a82,
|
||||
SFX,EDF036D6/1ED075D1,Music_1ED075D1,,Music,Music,,,10.909,48000,2,116934,c6116d638313c3e5,
|
||||
SFX,EDF036D6/1ED19140,Music_1ED19140,,Music,Music,,,118.144,48000,2,1977830,cd18434c96a33371,
|
||||
SFX,EDF036D6/1EDB523D,Music_1EDB523D,,Music,Music,,,57.989,48000,2,978464,a9303a561db1736c,
|
||||
SFX,EDF036D6/1F44015A,Music_1F44015A,,Music,Music,,,150.000,48000,2,2235862,ff86dc19c70f97be,
|
||||
SFX,EDF036D6/1F889215,Music_1F889215,,Music,Music,,,120.674,48000,2,1962204,d582d0342bc42104,
|
||||
SFX,EDF036D6/1FFD945F,Music_1FFD945F,,Music,Music,,,18.045,48000,2,259821,4aeb30f7098adbbf,
|
||||
SFX,EDF036D6/204449FE,Music_204449FE,,Music,Music,,,49.892,48000,2,717441,b1db8172723811e4,
|
||||
SFX,EDF036D6/2071230E,Music_2071230E,,Music,Music,,,99.736,48000,2,1514907,c21bde31f78fd34a,
|
||||
SFX,EDF036D6/20857A8E,Music_20857A8E,,Music,Music,,,109.981,48000,2,1635385,1cfde52c13e04959,
|
||||
SFX,EDF036D6/2105D597,Music_2105D597,,Music,Music,,,9.600,48000,2,98961,557f70acbbcfc2fe,
|
||||
SFX,EDF036D6/2113CD65,Music_2113CD65,,Music,Music,,,112.514,48000,2,1941069,90c37a7953826bff,
|
||||
SFX,EDF036D6/2199A947,Music_2199A947,,Music,Music,,,124.212,48000,2,1946256,b94df883b92bf6c7,
|
||||
SFX,EDF036D6/2239696F,Music_2239696F,,Music,Music,,,14.659,48000,2,222484,899cd11416591458,
|
||||
SFX,EDF036D6/22C833C6,Music_22C833C6,,Music,Music,,,41.468,48000,2,586179,8e5ece33b943d4de,
|
||||
SFX,EDF036D6/23207A9C,Music_23207A9C,,Music,Music,,,150.000,48000,2,2261291,4f45d4cb0fe64351,
|
||||
SFX,EDF036D6/23F9F43D,Music_23F9F43D,,Music,Music,,,100.231,48000,2,1638853,f9e28ef5ed84a60a,
|
||||
SFX,EDF036D6/242766B2,Music_242766B2,,Music,Music,,,32.891,48000,2,515906,916faaffe6c5803d,
|
||||
SFX,EDF036D6/2448BD91,Music_2448BD91,,Music,Music,,,5.724,48000,2,83185,d115a598c998aef9,
|
||||
SFX,EDF036D6/246625C9,Music_246625C9,,Music,Music,,,114.167,48000,2,1823209,fa38deac3aa77635,
|
||||
SFX,EDF036D6/24FEAAA6,Music_24FEAAA6,,Music,Music,,,4.999,48000,2,68925,fdccc549d81b3b54,
|
||||
SFX,EDF036D6/25A00909,Music_25A00909,,Music,Music,,,4.007,48000,2,56999,30c67fa1ef9c0000,
|
||||
SFX,EDF036D6/25A7D9DB,Music_25A7D9DB,,Music,Music,,,49.876,48000,2,719556,7b4ef5b74c5fcc22,
|
||||
SFX,EDF036D6/25D08F49,Music_25D08F49,,Music,Music,,,5.241,48000,2,79549,1e6a7c7129cb3843,
|
||||
SFX,EDF036D6/25F4F497,Music_25F4F497,,Music,Music,,,8.971,48000,2,147262,6e20fc042dd125cf,
|
||||
SFX,EDF036D6/263E3B2E,Music_263E3B2E,,Music,Music,,,120.674,48000,2,1962177,0f512c818b122acf,
|
||||
SFX,EDF036D6/26F6662B,Music_26F6662B,,Music,Music,,,84.268,48000,2,1252553,937b034854575c25,
|
||||
SFX,EDF036D6/2714A216,Music_2714A216,,Music,Music,,,124.043,48000,2,1981814,289af901611c4269,
|
||||
SFX,EDF036D6/27B481B5,Music_27B481B5,,Music,Music,,,42.180,48000,2,590926,6aee62ad4d7686de,
|
||||
SFX,EDF036D6/286EAFBC,Music_286EAFBC,,Music,Music,,,25.576,48000,2,464876,01f9a32a5c85562c,
|
||||
SFX,EDF036D6/291F6742,Music_291F6742,,Music,Music,,,52.323,48000,2,804372,425f49258782e7ac,
|
||||
SFX,EDF036D6/29390786,Music_29390786,,Music,Music,,,6.661,48000,2,101067,7c84fa8ca1da620d,
|
||||
SFX,EDF036D6/294C967B,Music_294C967B,,Music,Music,,,11.322,48000,2,161017,ca393e47bc8ecbd1,
|
||||
SFX,EDF036D6/296B28EE,Music_296B28EE,,Music,Music,,,71.581,48000,2,1018253,511aba3e78e85eee,
|
||||
SFX,EDF036D6/2987BACD,Music_2987BACD,,Music,Music,,,83.742,48000,2,1246073,9c48274110f1a12b,
|
||||
SFX,EDF036D6/29AB41BA,Music_29AB41BA,,Music,Music,,,60.123,48000,2,1018528,6f56ef771f3434f6,
|
||||
SFX,EDF036D6/29D2F637,Music_29D2F637,,Music,Music,,,203.312,48000,2,2971838,ec89b9e769b35868,
|
||||
SFX,EDF036D6/2A1AD885,Music_2A1AD885,,Music,Music,,,34.675,48000,2,520729,0d0c912a47f702c1,
|
||||
SFX,EDF036D6/2A24CEA6,Music_2A24CEA6,,Music,Music,,,1.409,48000,2,22216,677f3a8d58205de5,
|
||||
SFX,EDF036D6/2AA3CEA0,Music_2AA3CEA0,,Music,Music,,,26.182,48000,2,418694,ed57512cccf5c2be,
|
||||
SFX,EDF036D6/2AD309E6,Music_2AD309E6,,Music,Music,,,25.576,48000,2,369294,bda56bef71032f88,
|
||||
SFX,EDF036D6/2B07A62F,Music_2B07A62F,,Music,Music,,,5.429,48000,2,82642,f7158a1afd44959f,
|
||||
SFX,EDF036D6/2B65303C,Music_2B65303C,,Music,Music,,,119.221,48000,2,1463918,e69ddb4be1b7b84b,
|
||||
SFX,EDF036D6/2BFCA0A1,Music_2BFCA0A1,,Music,Music,,,117.000,48000,2,1375538,fe76f756eab10a0d,
|
||||
SFX,EDF036D6/2C8CEB90,Music_2C8CEB90,,Music,Music,,,53.066,48000,2,310850,ba12cb8c0ce2c213,
|
||||
SFX,EDF036D6/2CC1F4E1,Music_2CC1F4E1,,Music,Music,,,18.177,48000,2,259134,d5a00a1674c95380,
|
||||
SFX,EDF036D6/2CDA63DE,Music_2CDA63DE,,Music,Music,,,119.035,48000,2,1815581,bf1a62d940e5ca88,
|
||||
SFX,EDF036D6/2CE4147F,Music_2CE4147F,,Music,Music,,,19.963,48000,2,282315,81f2136e11b85bdd,
|
||||
SFX,EDF036D6/2D31EA09,Music_2D31EA09,,Music,Music,,,25.672,48000,2,380558,a69d5fa1bade8753,
|
||||
SFX,EDF036D6/2D321DF5,Music_2D321DF5,,Music,Music,,,9.600,48000,2,143544,f93fca0d047cd2fe,
|
||||
SFX,EDF036D6/2DB6F423,Music_2DB6F423,,Music,Music,,,136.744,48000,2,1552591,5a5ff2c4f953aeac,
|
||||
SFX,EDF036D6/2DF40EC5,Music_2DF40EC5,,Music,Music,,,124.212,48000,2,2081712,72d855be03a60acf,
|
||||
SFX,EDF036D6/2E49AEFC,Music_2E49AEFC,,Music,Music,,,43.254,48000,2,643737,a2199aa674d1a491,
|
||||
SFX,EDF036D6/2E885FD0,Music_2E885FD0,,Music,Music,,,9.351,48000,2,147447,2eaf9dadf364eae3,
|
||||
SFX,EDF036D6/2F0BF544,Music_2F0BF544,,Music,Music,,,10.712,48000,2,163834,14e6e64f1f87b86c,
|
||||
SFX,EDF036D6/2FBB03E0,Music_2FBB03E0,,Music,Music,,,8.196,48000,2,116776,d9bc1e573ee6d1a3,
|
||||
SFX,EDF036D6/301C76CA,Music_301C76CA,,Music,Music,,,57.931,48000,2,990419,02d9807cc745d4f2,
|
||||
SFX,EDF036D6/3057FE36,Music_3057FE36,,Music,Music,,,9.790,48000,2,136096,9c32781ab97fe445,
|
||||
SFX,EDF036D6/3085A581,Music_3085A581,,Music,Music,,,20.631,48000,2,357823,f5f4386a4f22b986,
|
||||
SFX,EDF036D6/319FC00C,Music_319FC00C,,Music,Music,,,76.736,48000,2,1300493,d2288e6324088412,
|
||||
SFX,EDF036D6/31AB3252,Music_31AB3252,,Music,Music,,,1.439,48000,2,24279,9e1452b253113e97,
|
||||
SFX,EDF036D6/31C2F642,Music_31C2F642,,Music,Music,,,13.165,48000,2,185823,1bc15699e00200fe,
|
||||
SFX,EDF036D6/322DC5AF,Music_322DC5AF,,Music,Music,,,4.388,48000,2,62342,7bf7872bc73c9174,
|
||||
SFX,EDF036D6/32DCAEE7,Music_32DCAEE7,,Music,Music,,,9.306,48000,2,131764,9e9a2794e9fe45a1,
|
||||
SFX,EDF036D6/32EB3DA7,Music_32EB3DA7,,Music,Music,,,171.524,48000,2,2600951,2066f38866c5d960,
|
||||
SFX,EDF036D6/33057EFC,Music_33057EFC,,Music,Music,,,68.386,48000,2,972781,d7c24aa3f26ceca9,
|
||||
SFX,EDF036D6/33695FB1,Music_33695FB1,,Music,Music,,,4.000,48000,2,64991,1808f37aef2d95cb,
|
||||
SFX,EDF036D6/33828946,Music_33828946,,Music,Music,,,6.005,48000,2,98962,7dc7fbfc08e0f669,
|
||||
SFX,EDF036D6/3418F5CF,Music_3418F5CF,,Music,Music,,,6.045,48000,2,90863,cc9cf509df1c7a96,
|
||||
SFX,EDF036D6/3452B01B,Music_3452B01B,,Music,Music,,,114.167,48000,2,1925579,d0e77781234a58f8,
|
||||
SFX,EDF036D6/3473C68E,Music_3473C68E,,Music,Music,,,21.231,48000,2,329799,1556993ad3defc8e,
|
||||
SFX,EDF036D6/350357ED,Music_350357ED,,Music,Music,,,12.850,48000,2,152754,effedf711799f9fb,
|
||||
SFX,EDF036D6/356CBA43,Music_356CBA43,,Music,Music,,,7.836,48000,2,59556,47e3f32bc4d4a5cc,
|
||||
SFX,EDF036D6/3584EC39,Music_3584EC39,,Music,Music,,,51.176,48000,2,768089,0cf2d9590cbbcdd6,
|
||||
SFX,EDF036D6/36FBFB5B,Music_36FBFB5B,,Music,Music,,,14.560,48000,6,264912,800fdb94db6dfeec,
|
||||
SFX,EDF036D6/372964C4,Music_372964C4,,Music,Music,,,5.887,48000,2,86948,f6d4d75ab8445bbd,
|
||||
SFX,EDF036D6/38597057,Music_38597057,,Music,Music,,,27.239,44100,2,355958,9d0730c4a1c127da,
|
||||
SFX,EDF036D6/392269E5,Music_392269E5,,Music,Music,,,9.166,48000,2,136727,28667f4a68ee8e66,
|
||||
SFX,EDF036D6/393BC53A,Music_393BC53A,,Music,Music,,,171.524,48000,6,7192714,eba537acd74009ba,
|
||||
SFX,EDF036D6/39EF16A1,Music_39EF16A1,,Music,Music,,,20.571,48000,2,322708,cbc57a438236bfcd,
|
||||
SFX,EDF036D6/3A0F34AA,Music_3A0F34AA,,Music,Music,,,20.842,48000,2,327714,4feb9c5e467509b3,
|
||||
SFX,EDF036D6/3A6E0069,Music_3A6E0069,,Music,Music,,,30.607,48000,2,440966,23f0f02258eb7c3f,
|
||||
SFX,EDF036D6/3A96A66A,Music_3A96A66A,,Music,Music,,,5.426,48000,2,78334,1a7e9e3fd65324a8,
|
||||
SFX,EDF036D6/3AB86854,Music_3AB86854,,Music,Music,,,124.212,48000,2,2186737,956cd355f2baec4d,
|
||||
SFX,EDF036D6/3B19926C,Music_3B19926C,,Music,Music,,,32.360,48000,2,571396,f8cfcdc11c88aff9,
|
||||
SFX,EDF036D6/3B1C4571,Music_3B1C4571,,Music,Music,,,26.182,48000,2,456132,e1e55cfe4e98a17b,
|
||||
SFX,EDF036D6/3B276E4D,Music_3B276E4D,,Music,Music,,,125.626,48000,2,1911354,0f17594e2df36276,
|
||||
SFX,EDF036D6/3B30E8B5,Music_3B30E8B5,,Music,Music,,,4.422,48000,2,72779,b73c25399a8c66c7,
|
||||
SFX,EDF036D6/3BA5398D,Music_3BA5398D,,Music,Music,,,6.028,48000,2,85229,f7d80b8c2a5d23ec,
|
||||
SFX,EDF036D6/3C106E75,Music_3C106E75,,Music,Music,,,6.527,48000,2,95702,8f0ccbaa0deb002e,
|
||||
SFX,EDF036D6/3C961A0D,Music_3C961A0D,,Music,Music,,,96.021,48000,2,1405544,5b6c24b48087e2b3,
|
||||
SFX,EDF036D6/3CBF4761,Music_3CBF4761,,Music,Music,,,4.856,48000,2,70027,d408c7bb17835303,
|
||||
SFX,EDF036D6/3CE33965,Music_3CE33965,,Music,Music,,,4.382,48000,2,64257,1fd2e7f5eecfeecb,
|
||||
SFX,EDF036D6/3D54599E,Music_3D54599E,,Music,Music,,,13.390,48000,2,187301,27fed970b91d3ca8,
|
||||
SFX,EDF036D6/3E47446A,Music_3E47446A,,Music,Music,,,7.806,48000,2,118924,c8fd851d3d5e5ea8,
|
||||
SFX,EDF036D6/3F04E423,Music_3F04E423,,Music,Music,,,128.764,48000,2,2039407,4ed2d6eaa4c10c65,
|
||||
SFX,EDF036D6/3F312E20,Music_3F312E20,,Music,Music,,,57.040,48000,2,809641,cb602f293c127d15,
|
||||
SFX,EDF036D6/3FFB5B14,Music_3FFB5B14,,Music,Music,,,9.547,48000,2,142043,f859153f7497528d,
|
||||
SFX,F16E832D:0,SEQ_DemonTrials_1,SEQ_DemonTrials#1,SFX,Scripted sequences,,,17.280,48000,1,105109,316e989c34a90718,
|
||||
SFX,F16E832D:1,SEQ_DemonTrials_2,SEQ_DemonTrials#2,SFX,Scripted sequences,,,13.525,48000,1,84855,629e4ae11d2af39c,
|
||||
SFX,F16E832D:2,SEQ_DemonTrials_3,SEQ_DemonTrials#3,SFX,Scripted sequences,,,10.837,48000,1,65652,56430a3bb8bcadfd,
|
||||
SFX,F16E832D:3,SEQ_DemonTrials_4,SEQ_DemonTrials#4,SFX,Scripted sequences,,,14.635,48000,1,91112,44b6dd9a637cfdc7,
|
||||
SFX,F16E832D:4,SEQ_DemonTrials_5,SEQ_DemonTrials#5,SFX,Scripted sequences,,,12.032,48000,1,74765,554d0d717858a5a6,
|
||||
SFX,F16E832D:5,SEQ_DemonTrials_6,SEQ_DemonTrials#6,SFX,Scripted sequences,,,17.707,48000,1,107955,7462b3589a40f0e4,
|
||||
SFX,F16E832D:6,SEQ_DemonTrials_7,SEQ_DemonTrials#7,SFX,Scripted sequences,,,7.723,48000,1,47185,fbc1fea07a56b18e,
|
||||
SFX,F16E832D:7,SEQ_DemonTrials_8,SEQ_DemonTrials#8,SFX,Scripted sequences,,,15.232,48000,1,91634,7f84cdc41d59c8ed,
|
||||
SFX,F16E832D:8,SEQ_DemonTrials_9,SEQ_DemonTrials#9,SFX,Scripted sequences,,,14.635,48000,1,89392,d3771bede86e3233,
|
||||
SFX,F1B81FC7,PVOL_Water_Exterior,PVOL_Water_Exterior_1,Ambience,Room tone,,,25.722,32000,2,261962,55e9f9cd4bd498f2,
|
||||
SFX,F9A51D18:0,AMB_OW_C46_1,AMB_OW_C46#1,Ambience,Ambience,,,84.593,32000,4,2210553,3b7926711dcd2996,
|
||||
SFX,F9A51D18:1,AMB_OW_C46_2,AMB_OW_C46#2,Ambience,Ambience,,,59.741,32000,4,1546334,c9cb4e7868f6a813,
|
||||
SFX,F9A51D18:2,AMB_OW_C46_3,AMB_OW_C46#3,Ambience,Ambience,,,97.923,32000,4,2728789,e6be03d0fe2c6639,
|
||||
SFX,FCA4F778/0EC73393,FCA4F778_0EC73393,,Unknown,Unknown,,,56.689,48000,2,843627,8bce3f68a65ae479,
|
||||
SFX,FD4461EA:0,AMB_RiddlerChallenge1_1,AMB_RiddlerChallenge1#1,Ambience,Ambience,,,35.157,32000,4,828243,506937146f3bb8bd,
|
||||
SFX,FD4461EA:1,AMB_RiddlerChallenge1_2,AMB_RiddlerChallenge1#2,Ambience,Ambience,,,55.551,32000,4,1462792,1239f45c45e2f82a,
|
||||
SFX,FD4461EA:2,AMB_RiddlerChallenge1_3,AMB_RiddlerChallenge1#3,Ambience,Ambience,,,16.384,32000,4,377945,d42730fbd1ecdcc3,
|
||||
SFX,FDB108B0,SEQ_Steel_C5_WaterDoors,SEQ_Steel_C5_WaterDoors_1,SFX,Scripted sequences,,,17.285,48000,2,228831,bea79a6d7fe41df0,
|
||||
SFX,FFF56412,AMB_UnderworldC2_V2,AMB_UnderworldC2_V2_1,Ambience,Ambience,,,28.160,32000,4,580489,ee66e8d0f24876e6,
|
||||
SFX,01EB5DDC:0,AMB_OW_B_37_1,AMB_OW_B_37#1,Ambience,Ambience,,,126.880,32000,4,3753364,079885b8f4146578,
|
||||
SFX,01EB5DDC:1,AMB_OW_B_37_2,AMB_OW_B_37#2,Ambience,Ambience,,,95.915,32000,4,2825273,f2feb7d5c975cb29,
|
||||
SFX,01EB5DDC:2,AMB_OW_B_37_3,AMB_OW_B_37#3,Ambience,Ambience,,,71.680,32000,4,2093574,4cb79bfe20e6e46e,
|
||||
SFX,01EB5DDC:3,AMB_OW_B_37_4,AMB_OW_B_37#4,Ambience,Ambience,,,73.720,32000,4,2176579,c671caa71010cc41,
|
||||
SFX,01EB5DDC:4,AMB_OW_B_37_5,AMB_OW_B_37#5,Ambience,Ambience,,,112.224,32000,4,3257825,5e6d73dc0efee7ac,
|
||||
SFX,01EB5DDC:5,AMB_OW_B_37_6,AMB_OW_B_37#6,Ambience,Ambience,,,77.999,32000,4,2269598,72ca05c3ec831537,
|
||||
SFX,01EB5DDC:6,AMB_OW_B_37_7,AMB_OW_B_37#7,Ambience,Ambience,,,103.764,32000,4,3180437,e25931054a39debb,
|
||||
SFX,062748BA:0,AMB_GCPD_HQ_1,AMB_GCPD_HQ#1,Ambience,Ambience,,Cop,40.852,32000,4,1204406,a0a7812ae604c8e2,
|
||||
SFX,062748BA:1,AMB_GCPD_HQ_2,AMB_GCPD_HQ#2,Ambience,Ambience,,Cop,9.714,32000,4,218213,375fb85c4d413d51,
|
||||
SFX,0B18F44C:0,CHR_RasBossFight_1,CHR_RasBossFight#1,SFX,Character foley,,,17.280,48000,1,105109,316e989c34a90718,
|
||||
SFX,0B18F44C:1,CHR_RasBossFight_2,CHR_RasBossFight#2,SFX,Character foley,,,13.525,48000,1,84855,629e4ae11d2af39c,
|
||||
SFX,0B18F44C:2,CHR_RasBossFight_3,CHR_RasBossFight#3,SFX,Character foley,,,14.635,48000,1,91112,44b6dd9a637cfdc7,
|
||||
SFX,0B18F44C:3,CHR_RasBossFight_4,CHR_RasBossFight#4,SFX,Character foley,,,12.032,48000,1,74765,554d0d717858a5a6,
|
||||
SFX,0B18F44C:4,CHR_RasBossFight_5,CHR_RasBossFight#5,SFX,Character foley,,,7.486,48000,4,223830,e8915027e3ce55d0,
|
||||
SFX,0E398B80:0,SEQ_OW_Pay_Your_Respects_1,SEQ_OW_Pay_Your_Respects#1,SFX,Scripted sequences,,,17.158,24000,2,105596,ca9d7921fc5d1235,
|
||||
SFX,0E398B80:1,SEQ_OW_Pay_Your_Respects_2,SEQ_OW_Pay_Your_Respects#2,SFX,Scripted sequences,,,17.158,24000,2,113215,0efb16b6e65dfb1f,
|
||||
SFX,10CF081C:0,AMB_OW_A_ALL_1,AMB_OW_A_ALL#1,Ambience,Ambience,,,100.000,32000,4,2881899,6eda738d26302232,
|
||||
SFX,10CF081C:1,AMB_OW_A_ALL_2,AMB_OW_A_ALL#2,Ambience,Ambience,,,93.757,32000,4,2767748,ec137126ce5315c3,
|
||||
SFX,10CF081C:2,AMB_OW_A_ALL_3,AMB_OW_A_ALL#3,Ambience,Ambience,,,49.740,32000,4,1473273,0af778e258914efc,
|
||||
SFX,10CF081C:3,AMB_OW_A_ALL_4,AMB_OW_A_ALL#4,Ambience,Ambience,,,90.494,32000,4,2647321,df324637be55b522,
|
||||
SFX,10CF081C:4,AMB_OW_A_ALL_5,AMB_OW_A_ALL#5,Ambience,Ambience,,,92.101,32000,4,2655287,87c065e2406e711a,
|
||||
SFX,10CF081C:5,AMB_OW_A_ALL_6,AMB_OW_A_ALL#6,Ambience,Ambience,,,89.000,32000,4,2587371,4b2939c4c48d3a37,
|
||||
SFX,10CF081C:6,AMB_OW_A_ALL_7,AMB_OW_A_ALL#7,Ambience,Ambience,,,87.963,32000,4,2452402,c48eac4d33968c11,
|
||||
SFX,10CF081C:7,AMB_OW_A_ALL_8,AMB_OW_A_ALL#8,Ambience,Ambience,,,48.736,32000,4,1510563,371103729d371d2f,
|
||||
SFX,10CF081C:8,AMB_OW_A_ALL_9,AMB_OW_A_ALL#9,Ambience,Ambience,,,112.224,32000,4,3257825,5e6d73dc0efee7ac,
|
||||
SFX,10CF081C:9,AMB_OW_A_ALL_10,AMB_OW_A_ALL#10,Ambience,Ambience,,,76.219,32000,4,2208864,b223019277b088c7,
|
||||
SFX,10CF081C:10,AMB_OW_A_ALL_11,AMB_OW_A_ALL#11,Ambience,Ambience,,,89.348,32000,4,2648238,780fcdfc367ef989,
|
||||
SFX,10CF081C:11,AMB_OW_A_ALL_12,AMB_OW_A_ALL#12,Ambience,Ambience,,,84.039,32000,4,2445442,d6ec35703e9f8a90,
|
||||
SFX,17B983F5:0,AMB_OW_E123478_1,AMB_OW_E123478#1,Ambience,Ambience,,,100.000,32000,4,2881899,6eda738d26302232,
|
||||
SFX,17B983F5:1,AMB_OW_E123478_2,AMB_OW_E123478#2,Ambience,Ambience,,,93.757,32000,4,2767748,ec137126ce5315c3,
|
||||
SFX,17B983F5:2,AMB_OW_E123478_3,AMB_OW_E123478#3,Ambience,Ambience,,,60.828,32000,4,1813753,1a1753a70c841dec,
|
||||
SFX,17B983F5:3,AMB_OW_E123478_4,AMB_OW_E123478#4,Ambience,Ambience,,,49.740,32000,4,1473273,0af778e258914efc,
|
||||
SFX,17B983F5:4,AMB_OW_E123478_5,AMB_OW_E123478#5,Ambience,Ambience,,,3.204,32000,2,38134,8cf522d67fc03fe7,
|
||||
SFX,17B983F5:5,AMB_OW_E123478_6,AMB_OW_E123478#6,Ambience,Ambience,,,59.559,32000,4,1734273,ace4cf02968e016a,
|
||||
SFX,17B983F5:6,AMB_OW_E123478_7,AMB_OW_E123478#7,Ambience,Ambience,,,90.494,32000,4,2647321,df324637be55b522,
|
||||
SFX,17B983F5:7,AMB_OW_E123478_8,AMB_OW_E123478#8,Ambience,Ambience,,,92.101,32000,4,2655287,87c065e2406e711a,
|
||||
SFX,17B983F5:8,AMB_OW_E123478_9,AMB_OW_E123478#9,Ambience,Ambience,,,87.963,32000,4,2452402,c48eac4d33968c11,
|
||||
SFX,17B983F5:9,AMB_OW_E123478_10,AMB_OW_E123478#10,Ambience,Ambience,,,48.736,32000,4,1510563,371103729d371d2f,
|
||||
SFX,17B983F5:10,AMB_OW_E123478_11,AMB_OW_E123478#11,Ambience,Ambience,,,4.205,32000,2,49274,acec2dcd750d3f1a,
|
||||
SFX,17B983F5:11,AMB_OW_E123478_12,AMB_OW_E123478#12,Ambience,Ambience,,,112.224,32000,4,3257825,5e6d73dc0efee7ac,
|
||||
SFX,17B983F5:12,AMB_OW_E123478_13,AMB_OW_E123478#13,Ambience,Ambience,,,76.219,32000,4,2208864,b223019277b088c7,
|
||||
SFX,17B983F5:13,AMB_OW_E123478_14,AMB_OW_E123478#14,Ambience,Ambience,,,89.348,32000,4,2648238,780fcdfc367ef989,
|
||||
SFX,17B983F5:14,AMB_OW_E123478_15,AMB_OW_E123478#15,Ambience,Ambience,,,84.039,32000,4,2445442,d6ec35703e9f8a90,
|
||||
SFX,1C305431,AMB_MadHatter_PostFight,AMB_MadHatter_PostFight_1,Ambience,Ambience,,Mad Hatter,6.003,32000,4,168722,1842bba5f1fb4194,
|
||||
SFX,1F3ABBB4:0,AMB_Scarecrow_Boat_1,AMB_Scarecrow_Boat#1,Ambience,Ambience,,,30.358,32000,2,347585,34b4d9a05154fa63,
|
||||
SFX,1F3ABBB4:1,AMB_Scarecrow_Boat_2,AMB_Scarecrow_Boat#2,Ambience,Ambience,,,29.326,32000,2,316292,52819d7ad292a6a6,
|
||||
SFX,23F6A8F8:0,AMB_CourtB1_1,AMB_CourtB1#1,Ambience,Ambience,,,14.368,32000,4,392497,923b77449efcd99e,
|
||||
SFX,23F6A8F8:1,AMB_CourtB1_2,AMB_CourtB1#2,Ambience,Ambience,,,19.371,32000,4,527286,51acfa6500a7486d,
|
||||
SFX,23F6A8F8:2,AMB_CourtB1_3,AMB_CourtB1#3,Ambience,Ambience,,,27.000,32000,4,755899,af2ec78c4279be78,
|
||||
SFX,247E31CA,CHR_Grundy,CHR_Grundy_1,SFX,Character foley,,Solomon Grundy,3.284,48000,2,34305,2271da11e324c823,
|
||||
SFX,2DE53B3F:0,AMB_OW_Tower_Exterior_1,AMB_OW_Tower_Exterior#1,Ambience,Ambience,,,57.597,32000,4,1730923,a0b3071f0df4d576,
|
||||
SFX,2DE53B3F:1,AMB_OW_Tower_Exterior_2,AMB_OW_Tower_Exterior#2,Ambience,Ambience,,,42.956,32000,4,1283371,fcc511bca155e988,
|
||||
SFX,2DE53B3F:2,AMB_OW_Tower_Exterior_3,AMB_OW_Tower_Exterior#3,Ambience,Ambience,,,59.559,32000,4,1734273,ace4cf02968e016a,
|
||||
SFX,2DE53B3F:3,AMB_OW_Tower_Exterior_4,AMB_OW_Tower_Exterior#4,Ambience,Ambience,,,8.319,24000,1,21247,fe0c508a6d2a341c,
|
||||
SFX,2DE53B3F:4,AMB_OW_Tower_Exterior_5,AMB_OW_Tower_Exterior#5,Ambience,Ambience,,,98.450,32000,4,2836823,1f2b7810a3c875a0,
|
||||
SFX,2DE53B3F:5,AMB_OW_Tower_Exterior_6,AMB_OW_Tower_Exterior#6,Ambience,Ambience,,,6.254,24000,1,20450,03df69ec6485a152,
|
||||
SFX,2DE53B3F:6,AMB_OW_Tower_Exterior_7,AMB_OW_Tower_Exterior#7,Ambience,Ambience,,,5.436,24000,1,13147,419e24dbccf1e7e5,
|
||||
SFX,2DE53B3F:7,AMB_OW_Tower_Exterior_8,AMB_OW_Tower_Exterior#8,Ambience,Ambience,,,6.594,24000,1,18166,15bc9f238253efa2,
|
||||
SFX,2DE53B3F:8,AMB_OW_Tower_Exterior_9,AMB_OW_Tower_Exterior#9,Ambience,Ambience,,,7.198,24000,1,17394,ad8713b562f1714b,
|
||||
SFX,2DE53B3F:9,AMB_OW_Tower_Exterior_10,AMB_OW_Tower_Exterior#10,Ambience,Ambience,,,7.060,24000,1,22630,d8465ba3a42b9387,
|
||||
SFX,2DE53B3F:10,AMB_OW_Tower_Exterior_11,AMB_OW_Tower_Exterior#11,Ambience,Ambience,,,8.421,24000,1,24860,ee66e3e09bf1dc79,
|
||||
SFX,396E6EBD:0,AMB_Church_B1_1,AMB_Church_B1#1,Ambience,Ambience,,,18.324,32000,4,496213,5eb76e08e5346ab4,
|
||||
SFX,396E6EBD:1,AMB_Church_B1_2,AMB_Church_B1#2,Ambience,Ambience,,,11.720,32000,4,299346,7576251df0911437,
|
||||
SFX,3CB673E7,AMB_HushRoom,AMB_HushRoom_1,Ambience,Ambience,,,24.000,32000,4,671357,1279af0238e7afdc,
|
||||
SFX,3F4B608F,AMB_Underworld_S1,AMB_Underworld_S1_1,Ambience,Ambience,,,15.536,32000,4,455276,9fb735c477db3804,
|
||||
SFX,45A19974:0,AMB_RumbleDebris_1,AMB_RumbleDebris#1,Ambience,Ambience,,,15.495,32000,4,404444,646660bf0b7207b1,
|
||||
SFX,45A19974:1,AMB_RumbleDebris_2,AMB_RumbleDebris#2,Ambience,Ambience,,,11.602,32000,4,321943,54474dbc608a1506,
|
||||
SFX,45A19974:2,AMB_RumbleDebris_3,AMB_RumbleDebris#3,Ambience,Ambience,,,9.056,32000,4,239594,7fc588270d062fc5,
|
||||
SFX,45A19974:3,AMB_RumbleDebris_4,AMB_RumbleDebris#4,Ambience,Ambience,,,8.942,32000,4,255627,8ebabec798972470,
|
||||
SFX,4D4B76FB:0,AMB_Underworld_A3_1,AMB_Underworld_A3#1,Ambience,Ambience,,,15.691,28000,1,98560,19c86c48a0681287,
|
||||
SFX,4D4B76FB:1,AMB_Underworld_A3_2,AMB_Underworld_A3#2,Ambience,Ambience,,,15.263,28000,1,98065,b7f3b6ef76594a50,
|
||||
SFX,4D4B76FB:2,AMB_Underworld_A3_3,AMB_Underworld_A3#3,Ambience,Ambience,,,10.682,28000,1,69865,d2ef62f37b65f62f,
|
||||
SFX,4D4B76FB:3,AMB_Underworld_A3_4,AMB_Underworld_A3#4,Ambience,Ambience,,,10.139,28000,1,61110,b3bbb946619eb519,
|
||||
SFX,4D4B76FB:4,AMB_Underworld_A3_5,AMB_Underworld_A3#5,Ambience,Ambience,,,9.415,28000,1,58433,7d6e7e3978db3156,
|
||||
SFX,4D4B76FB:5,AMB_Underworld_A3_6,AMB_Underworld_A3#6,Ambience,Ambience,,,15.716,28000,1,91342,4f47198c1322c19a,
|
||||
SFX,4D4B76FB:6,AMB_Underworld_A3_7,AMB_Underworld_A3#7,Ambience,Ambience,,,8.383,28000,1,54706,7cf7332a829e5065,
|
||||
SFX,4D4B76FB:7,AMB_Underworld_A3_8,AMB_Underworld_A3#8,Ambience,Ambience,,,13.995,32000,4,420389,44e2d6cb6bc05ecd,
|
||||
SFX,4DB8A342,OBJ_Tannoy,OBJ_Tannoy_1,SFX,Objects,,,2.229,32000,5,26334,d5aaba538d661493,
|
||||
SFX,4DF92D97:0,OBJ_GunCrates_1,OBJ_GunCrates#1,SFX,Objects,,,1.249,48000,2,17932,5fcb6961ce873316,
|
||||
SFX,4DF92D97:1,OBJ_GunCrates_2,OBJ_GunCrates#2,SFX,Objects,,,1.388,48000,2,19892,eb5c4c8018dcb3b2,
|
||||
SFX,4DF92D97:2,OBJ_GunCrates_3,OBJ_GunCrates#3,SFX,Objects,,,1.388,48000,2,19708,64e7be26e0731db0,
|
||||
SFX,4DF92D97:3,OBJ_GunCrates_4,OBJ_GunCrates#4,SFX,Objects,,,1.388,48000,2,23070,0314231fb31ebd12,
|
||||
SFX,4EE4DEAC,VOL_IceFreeze_Interior,VOL_IceFreeze_Interior_1,Ambience,Room tone,,,24.000,48000,2,519820,93388507ef36519c,
|
||||
SFX,4F4B79DF:0,AMB_Underworld_C1_1,AMB_Underworld_C1#1,Ambience,Ambience,,,10.062,32000,4,340307,1108533c7f5600f7,
|
||||
SFX,4F4B79DF:1,AMB_Underworld_C1_2,AMB_Underworld_C1#2,Ambience,Ambience,,,16.746,48000,4,497401,835dc96ff6b7a1f9,
|
||||
SFX,504B7BB4:0,AMB_Underworld_B5_1,AMB_Underworld_B5#1,Ambience,Ambience,,,4.977,24000,1,14907,2abb33cf9e8b4b4f,
|
||||
SFX,504B7BB4:1,AMB_Underworld_B5_2,AMB_Underworld_B5#2,Ambience,Ambience,,,6.875,24000,1,19276,ecd69d2dce5e37a7,
|
||||
SFX,504B7BB4:2,AMB_Underworld_B5_3,AMB_Underworld_B5#3,Ambience,Ambience,,,6.591,24000,1,18148,7e09b2d25c2eea3a,
|
||||
SFX,504B7BB4:3,AMB_Underworld_B5_4,AMB_Underworld_B5#4,Ambience,Ambience,,,4.287,24000,1,13144,bf602fa72cf41dfe,
|
||||
SFX,504B7BB4:4,AMB_Underworld_B5_5,AMB_Underworld_B5#5,Ambience,Ambience,,,7.211,24000,1,21936,c7a506f8097c8015,
|
||||
SFX,504B7BB4:5,AMB_Underworld_B5_6,AMB_Underworld_B5#6,Ambience,Ambience,,,3.002,24000,1,9132,09901bea0b7aeb67,
|
||||
SFX,504B7BB4:6,AMB_Underworld_B5_7,AMB_Underworld_B5#7,Ambience,Ambience,,,4.678,24000,1,14423,de40e62612dad2fd,
|
||||
SFX,504B7BB4:7,AMB_Underworld_B5_8,AMB_Underworld_B5#8,Ambience,Ambience,,,15.691,28000,1,98560,19c86c48a0681287,
|
||||
SFX,504B7BB4:8,AMB_Underworld_B5_9,AMB_Underworld_B5#9,Ambience,Ambience,,,15.263,28000,1,98065,b7f3b6ef76594a50,
|
||||
SFX,504B7BB4:9,AMB_Underworld_B5_10,AMB_Underworld_B5#10,Ambience,Ambience,,,10.682,28000,1,69865,d2ef62f37b65f62f,
|
||||
SFX,504B7BB4:10,AMB_Underworld_B5_11,AMB_Underworld_B5#11,Ambience,Ambience,,,5.649,24000,1,15919,7161ab2c29c0ad02,
|
||||
SFX,504B7BB4:11,AMB_Underworld_B5_12,AMB_Underworld_B5#12,Ambience,Ambience,,,10.139,28000,1,61110,b3bbb946619eb519,
|
||||
SFX,504B7BB4:12,AMB_Underworld_B5_13,AMB_Underworld_B5#13,Ambience,Ambience,,,9.415,28000,1,58433,7d6e7e3978db3156,
|
||||
SFX,504B7BB4:13,AMB_Underworld_B5_14,AMB_Underworld_B5#14,Ambience,Ambience,,,7.804,24000,1,22303,e97522fb2216e189,
|
||||
SFX,504B7BB4:14,AMB_Underworld_B5_15,AMB_Underworld_B5#15,Ambience,Ambience,,,2.753,24000,1,8077,43a3682b07706ee1,
|
||||
SFX,504B7BB4:15,AMB_Underworld_B5_16,AMB_Underworld_B5#16,Ambience,Ambience,,,15.536,32000,4,455276,9fb735c477db3804,
|
||||
SFX,504B7BB4:16,AMB_Underworld_B5_17,AMB_Underworld_B5#17,Ambience,Ambience,,,15.716,28000,1,91342,4f47198c1322c19a,
|
||||
SFX,504B7BB4:17,AMB_Underworld_B5_18,AMB_Underworld_B5#18,Ambience,Ambience,,,8.383,28000,1,54706,7cf7332a829e5065,
|
||||
SFX,504B7BB4:18,AMB_Underworld_B5_19,AMB_Underworld_B5#19,Ambience,Ambience,,,14.012,32000,4,442369,714ad5534983e096,
|
||||
SFX,504B7BB7,AMB_Underworld_B6,AMB_Underworld_B6_1,Ambience,Ambience,,,30.336,48000,4,902577,aaa3b73a68f71eb7,
|
||||
SFX,508590A0,AMB_HiddenAreas,AMB_HiddenAreas_1,Ambience,Ambience,,,18.666,32000,4,511603,10aefd8ea1557596,
|
||||
SFX,53C0E792,AMB_OW_D5,AMB_OW_D5_1,Ambience,Ambience,,,14.012,32000,4,419982,3bdaee4ebf08faad,
|
||||
SFX,53C0E793,AMB_OW_D4,AMB_OW_D4_1,Ambience,Ambience,,,14.012,32000,4,419982,3bdaee4ebf08faad,
|
||||
SFX,53C0E794,AMB_OW_D3,AMB_OW_D3_1,Ambience,Ambience,,,60.043,32000,4,1717445,16f8f80c393a2544,
|
||||
SFX,53C0E795,AMB_OW_D2,AMB_OW_D2_1,Ambience,Ambience,,,69.740,32000,4,2062109,17e7d4833c7dfae4,
|
||||
SFX,53C0E796:0,AMB_OW_D1_1,AMB_OW_D1#1,Ambience,Ambience,,,60.043,32000,4,1717445,16f8f80c393a2544,
|
||||
SFX,53C0E796:1,AMB_OW_D1_2,AMB_OW_D1#2,Ambience,Ambience,,,69.740,32000,4,2062109,17e7d4833c7dfae4,
|
||||
SFX,5445778E,AMB_Museum_Corridors,AMB_Museum_Corridors_1,Ambience,Ambience,,,28.571,32000,4,750171,f5549719c82a2af2,
|
||||
SFX,57572DAC/267F1DB3,Global_267F1DB3,,SFX,Global,,,10.092,24000,4,216184,c1f1c7385d15b611,
|
||||
SFX,65845381:0,AMB_MadHatter_Party_1,AMB_MadHatter_Party#1,Ambience,Ambience,,Mad Hatter,20.595,32000,4,582875,573c8558ebc5de5d,
|
||||
SFX,65845381:1,AMB_MadHatter_Party_2,AMB_MadHatter_Party#2,Ambience,Ambience,,Mad Hatter,35.144,32000,4,1047109,1bea95a6a07f9150,
|
||||
SFX,6866CE3A,WPN_Grundy,WPN_Grundy_1,SFX,Weapons,,Solomon Grundy,5.325,48000,1,44570,6de71f59fab751c1,
|
||||
SFX,6C151539:0,6C151539_1,6C151539#1,Unknown,Unknown,,,1.000,48000,1,10356,e9e3007ebc41458b,
|
||||
SFX,6C151539:1,6C151539_2,6C151539#2,Unknown,Unknown,,,1.000,48000,1,14149,f36ba9b3afa4f6b3,
|
||||
SFX,6C151539:2,6C151539_3,6C151539#3,Unknown,Unknown,,,1.000,48000,1,13537,452d9e2784cb5c41,
|
||||
SFX,7506D11C,AMB_ZsaszB1,AMB_ZsaszB1_1,Ambience,Ambience,,,20.000,32000,4,545599,fbb73780df30e80c,
|
||||
SFX,7D6D8269,AMB_Museum_S1,AMB_Museum_S1_1,Ambience,Ambience,,,20.468,32000,4,574954,4ce1cea0c3c854f4,
|
||||
SFX,7D6D826A,AMB_Museum_S2,AMB_Museum_S2_1,Ambience,Ambience,,,20.468,32000,4,574954,4ce1cea0c3c854f4,
|
||||
SFX,7D71D951,AMB_HushSpooky,AMB_HushSpooky_1,Ambience,Ambience,,,24.000,32000,4,680215,823670f2a118d1af,
|
||||
SFX,7D848FF0:0,AMB_OW_B_12456_1,AMB_OW_B_12456#1,Ambience,Ambience,,,126.880,32000,4,3753364,079885b8f4146578,
|
||||
SFX,7D848FF0:1,AMB_OW_B_12456_2,AMB_OW_B_12456#2,Ambience,Ambience,,,57.597,32000,4,1730923,a0b3071f0df4d576,
|
||||
SFX,7D848FF0:2,AMB_OW_B_12456_3,AMB_OW_B_12456#3,Ambience,Ambience,,,42.956,32000,4,1283371,fcc511bca155e988,
|
||||
SFX,7D848FF0:3,AMB_OW_B_12456_4,AMB_OW_B_12456#4,Ambience,Ambience,,,60.828,32000,4,1813753,1a1753a70c841dec,
|
||||
SFX,7D848FF0:4,AMB_OW_B_12456_5,AMB_OW_B_12456#5,Ambience,Ambience,,,84.593,32000,4,2210553,3b7926711dcd2996,
|
||||
SFX,7D848FF0:5,AMB_OW_B_12456_6,AMB_OW_B_12456#6,Ambience,Ambience,,,68.204,32000,4,2019040,e3b9a81b5d764030,
|
||||
SFX,7D848FF0:6,AMB_OW_B_12456_7,AMB_OW_B_12456#7,Ambience,Ambience,,,59.559,32000,4,1734273,ace4cf02968e016a,
|
||||
SFX,7D848FF0:7,AMB_OW_B_12456_8,AMB_OW_B_12456#8,Ambience,Ambience,,,98.450,32000,4,2836823,1f2b7810a3c875a0,
|
||||
SFX,7D848FF0:8,AMB_OW_B_12456_9,AMB_OW_B_12456#9,Ambience,Ambience,,,59.741,32000,4,1546334,c9cb4e7868f6a813,
|
||||
SFX,7D848FF0:9,AMB_OW_B_12456_10,AMB_OW_B_12456#10,Ambience,Ambience,,,97.923,32000,4,2728789,e6be03d0fe2c6639,
|
||||
SFX,7D848FF0:10,AMB_OW_B_12456_11,AMB_OW_B_12456#11,Ambience,Ambience,,,80.661,32000,4,2399422,f349c20ba690c651,
|
||||
SFX,7D848FF0:11,AMB_OW_B_12456_12,AMB_OW_B_12456#12,Ambience,Ambience,,,77.999,32000,4,2269598,72ca05c3ec831537,
|
||||
|
3365
data/catalog/characters.csv
Normal file
3365
data/catalog/characters.csv
Normal file
File diff suppressed because it is too large
Load Diff
65
data/catalog/index.json
Normal file
65
data/catalog/index.json
Normal file
@@ -0,0 +1,65 @@
|
||||
{
|
||||
"DEU": {
|
||||
"wad": "BmGame/CookedPCConsole/German/FFB9E71B_LOC_DEU.wad",
|
||||
"fingerprint": "2464-93ff67901d8eb43b",
|
||||
"sounds": 10816,
|
||||
"groups": {
|
||||
"Speech": 10815,
|
||||
"Ambience": 1
|
||||
},
|
||||
"with_character": 3366
|
||||
},
|
||||
"ESN": {
|
||||
"wad": "BmGame/CookedPCConsole/Spanish(Spain)/0E07A43D_LOC_ESN.wad",
|
||||
"fingerprint": "2464-90591332cf02ddad",
|
||||
"sounds": 10816,
|
||||
"groups": {
|
||||
"Speech": 10815,
|
||||
"Ambience": 1
|
||||
},
|
||||
"with_character": 3366
|
||||
},
|
||||
"FRA": {
|
||||
"wad": "BmGame/CookedPCConsole/French(France)/134795B3_LOC_FRA.wad",
|
||||
"fingerprint": "2464-b4b42ed9b0c025cf",
|
||||
"sounds": 10816,
|
||||
"groups": {
|
||||
"Speech": 10815,
|
||||
"Ambience": 1
|
||||
},
|
||||
"with_character": 3366
|
||||
},
|
||||
"INT": {
|
||||
"wad": "BmGame/CookedPCConsole/English(US)/28CCF006_LOC_INT.wad",
|
||||
"fingerprint": "2464-09e6656cc3fe5209",
|
||||
"sounds": 10816,
|
||||
"groups": {
|
||||
"Speech": 10815,
|
||||
"Ambience": 1
|
||||
},
|
||||
"with_character": 3366
|
||||
},
|
||||
"ITA": {
|
||||
"wad": "BmGame/CookedPCConsole/Italian/49D84887_LOC_ITA.wad",
|
||||
"fingerprint": "2464-f667c0ac06942f00",
|
||||
"sounds": 10816,
|
||||
"groups": {
|
||||
"Speech": 10815,
|
||||
"Ambience": 1
|
||||
},
|
||||
"with_character": 3366
|
||||
},
|
||||
"SFX": {
|
||||
"wad": "BmGame/CookedPCConsole/SFX/17705D3E.wad",
|
||||
"fingerprint": "83-a700d4ee8d10ae1f",
|
||||
"sounds": 651,
|
||||
"groups": {
|
||||
"Ambience": 225,
|
||||
"Unknown": 6,
|
||||
"SFX": 31,
|
||||
"Music": 389
|
||||
},
|
||||
"with_character": 0
|
||||
},
|
||||
"_info": "unmodified game files"
|
||||
}
|
||||
46
data/catalog/source/ita/Alfred.txt
Normal file
46
data/catalog/source/ita/Alfred.txt
Normal file
@@ -0,0 +1,46 @@
|
||||
Speech_AUGMENTED_Challenge_Alf_1.wem
|
||||
Speech_AUGMENTED_Challenge_Alf_2.wem
|
||||
Speech_CH_ZERO_CH0_BW_ALF_Climb1_1.wem
|
||||
Speech_CH_ZERO_CH0_BW_ALF_Climb1_3.wem
|
||||
Speech_CH_ZERO_CH0_BW_ALF_Climb2_1.wem
|
||||
Speech_CH_ZERO_CH0_BW_ALF_Climb2_2.wem
|
||||
Speech_CH_ZERO_CH0_BW_ALF_Climb3_2.wem
|
||||
Speech_CH_ZERO_CH0_ENCRYPTION_CARD2_1.wem
|
||||
Speech_CH_ZERO_CH0_ENCRYPTION_CARD3_3.wem
|
||||
Speech_CHURCH_B1_Ch2_BM_ALF_Post_Gun_3.wem
|
||||
Speech_CHURCH_B1_Ch2_BM_ALF_Post_Gun_4.wem
|
||||
Speech_GCPD_CH3_BM_ALF_Codes_2.wem
|
||||
Speech_GCPD_CH3_BM_ALF_Codes_5.wem
|
||||
Speech_MAP_BM_ALF_CURE_INTRO_1.wem
|
||||
Speech_MAP_BM_ALF_CURE_INTRO_2.wem
|
||||
Speech_MUSEUM_B1_CH3_BM_ALF_ORA_Catchup_2.wem
|
||||
Speech_MUSEUM_B1_CH3_BM_ALF_ORA_Catchup_3.wem
|
||||
Speech_New_game_Alfred_1.wem
|
||||
Speech_New_help_Enter_court_1.wem
|
||||
Speech_SIDE_DEADSHOT_VIC1_BM_OR_CONV_2.wem
|
||||
Speech_SIDE_DEADSHOT_VIC1_BM_OR_CONV_5.wem
|
||||
Speech_SIDE_DEADSHOT_VIC1_BM_OR_CONV_6.wem
|
||||
Speech_STEEL_B1_Ch2_BM_ALF_find_Control_1.wem
|
||||
Speech_STEEL_B1_Ch2_BM_ALF_find_Control_2.wem
|
||||
Speech_STEEL_B1_Ch2_BM_ALF_find_Control_5.wem
|
||||
Speech_STEEL_B1_CH2_Goto_Doc_ALF_1.wem
|
||||
Speech_STEEL_C3_CH3_post_seq_BM_ALF_1.wem
|
||||
Speech_STEEL_C3_CH3_post_seq_BM_ALF_5.wem
|
||||
Speech_STREETS_A1_CH0_BM_ALF_1.wem
|
||||
Speech_STREETS_A1_CH0_BM_ALF_2.wem
|
||||
Speech_STREETS_A1_CH2_BM_ALF_Church_2.wem
|
||||
Speech_STREETS_A1_CH2_BM_Alf_Riddler_catchup_2.wem
|
||||
Speech_STREETS_A3_CH2_BM_ALF_findroute_1.wem
|
||||
Speech_STREETS_A3_CH2_BM_ALF_findroute_3.wem
|
||||
Speech_STREETS_A3_CH3_BM_Wake_Intro_1.wem
|
||||
Speech_STREETS_A3_CH3_BM_Wake_Intro_5.wem
|
||||
Speech_STREETS_A3_CH7_NEW_DILEMMA_2.wem
|
||||
Speech_STREETS_A3_CH7_NEW_DILEMMA_5.wem
|
||||
Speech_STREETS_A3_CH7_NEW_DILEMMA_8.wem
|
||||
Speech_STREETS_A3_CH7_NEW_DILEMMA_10.wem
|
||||
Speech_STREETS_A3_CollectGrapnelBoost_2.wem
|
||||
Speech_STREETS_A3_CollectGrapnelBoost_3.wem
|
||||
Speech_STREETS_A4_CH3_BM_ALF_restablish_comms_1.wem
|
||||
Speech_STREETS_A4_CH3_BM_ALF_restablish_comms_2.wem
|
||||
Speech_STREETS_A5_CH4_BM_ALF_UPDATE_2.wem
|
||||
Speech_STREETS_A5_CH4_BM_Line_Launch_req_2.wem
|
||||
10
data/catalog/source/ita/Azrael.txt
Normal file
10
data/catalog/source/ita/Azrael.txt
Normal file
@@ -0,0 +1,10 @@
|
||||
Speech_SIDE_AZRAEL_Final_1.wem
|
||||
Speech_SIDE_AZRAEL_Final_3.wem
|
||||
Speech_SIDE_AZRAEL_Final_4.wem
|
||||
Speech_SIDE_AZRAEL_Final_6.wem
|
||||
Speech_SIDE_AZRAEL_Final_7.wem
|
||||
Speech_SIDE_AZRAEL_Final_8.wem
|
||||
Speech_SIDE_AZRAEL_Meeting_1_1.wem
|
||||
Speech_SIDE_AZRAEL_Meeting_2_1.wem
|
||||
Speech_SIDE_AZRAEL_Meeting_3_1.wem
|
||||
Speech_SIDE_AZRAEL_Meeting_4_1.wem
|
||||
45
data/catalog/source/ita/Bane.txt
Normal file
45
data/catalog/source/ita/Bane.txt
Normal file
@@ -0,0 +1,45 @@
|
||||
Speech_COMBAT_BARKS_BANE_Hit_batman_1.wem
|
||||
Speech_COMBAT_BARKS_BANE_Hit_batman_2.wem
|
||||
Speech_COMBAT_BARKS_BANE_Hit_batman_3.wem
|
||||
Speech_COMBAT_BARKS_BANE_Hit_tyger_1.wem
|
||||
Speech_COMBAT_BARKS_BANE_Hit_tyger_2.wem
|
||||
Speech_COMBAT_BARKS_BANE_Hit_tyger_3.wem
|
||||
Speech_COMBAT_BARKS_BANE_Hit_tyger_4.wem
|
||||
Speech_COMBAT_BARKS_BANE_Hit_tyger_5.wem
|
||||
Speech_COMBAT_BARKS_BANE_Hit_tyger_6.wem
|
||||
Speech_COMBAT_BARKS_BANE_Hit_tyger_7.wem
|
||||
Speech_COMBAT_BARKS_BANE_Hit_tyger_8.wem
|
||||
Speech_SIDE_BANE_Bane_look_at_map_1.wem
|
||||
Speech_SIDE_BANE_Bane_look_at_map_2.wem
|
||||
Speech_SIDE_BANE_Bane_look_at_map_3.wem
|
||||
Speech_SIDE_BANE_Bane_look_at_map_4.wem
|
||||
Speech_SIDE_BANE_CAUGHT_CONV_1.wem
|
||||
Speech_SIDE_BANE_CAUGHT_CONV_4.wem
|
||||
Speech_SIDE_BANE_END_1.wem
|
||||
Speech_SIDE_BANE_END_2.wem
|
||||
Speech_SIDE_BANE_END_3.wem
|
||||
Speech_SIDE_BANE_END_5.wem
|
||||
Speech_SIDE_BANE_END_7.wem
|
||||
Speech_SIDE_BANE_END_9.wem
|
||||
Speech_SIDE_BANE_IDLES_1.wem
|
||||
Speech_SIDE_BANE_IDLES_2.wem
|
||||
Speech_SIDE_BANE_IDLES_3.wem
|
||||
Speech_SIDE_BANE_INTRO_POST_MUSEUM_1.wem
|
||||
Speech_SIDE_BANE_INTRO_POST_MUSEUM_3.wem
|
||||
Speech_SIDE_BANE_INTRO_POST_MUSEUM_4.wem
|
||||
Speech_SIDE_BANE_INTRO_POST_MUSEUM_7.wem
|
||||
Speech_SIDE_BANE_INTRO_POST_MUSEUM_9.wem
|
||||
Speech_SIDE_BANE_INTRO_PRE_MUSEUM_4.wem
|
||||
Speech_SIDE_BANE_INTRO_PRE_MUSEUM_5.wem
|
||||
Speech_SIDE_BANE_INTRO_PRE_MUSEUM_6.wem
|
||||
Speech_SIDE_BANE_INTRO_PRE_MUSEUM_9.wem
|
||||
Speech_SIDE_BANE_INTRO_PRE_MUSEUM_10.wem
|
||||
Speech_SIDE_BANE_Pre_crate_captured_idles_1.wem
|
||||
Speech_SIDE_BANE_Pre_crate_captured_idles_2.wem
|
||||
Speech_SIDE_BANE_Pre_tyger_conv_1.wem
|
||||
Speech_SIDE_BANE_Pre_tyger_conv_3.wem
|
||||
Speech_SIDE_BANE_STOP_1.wem
|
||||
Speech_SIDE_BANE_STOP_3.wem
|
||||
Speech_SIDE_BANE_STOP_4.wem
|
||||
Speech_SIDE_CAT_MEET_BANE_2.wem
|
||||
Speech_SIDE_CAT_MEET_BANE_3.wem
|
||||
789
data/catalog/source/ita/Batman.txt
Normal file
789
data/catalog/source/ita/Batman.txt
Normal file
@@ -0,0 +1,789 @@
|
||||
Speech_CH_ZERO_CH0_Airlock1_2.wem
|
||||
Speech_CH_ZERO_CH0_Airlock2_2.wem
|
||||
Speech_CH_ZERO_CH0_BW_ALF_Climb1_2.wem
|
||||
Speech_CH_ZERO_CH0_BW_ALF_Climb1_4.wem
|
||||
Speech_CH_ZERO_CH0_BW_ALF_Climb2_3.wem
|
||||
Speech_CH_ZERO_CH0_BW_ALF_Climb2_4.wem
|
||||
Speech_CH_ZERO_CH0_BW_ALF_Climb2_5.wem
|
||||
Speech_CH_ZERO_CH0_BW_ALF_Climb3_1.wem
|
||||
Speech_CH_ZERO_CH0_BW_Breathing_1.wem
|
||||
Speech_CH_ZERO_CH0_BW_Breathing_2.wem
|
||||
Speech_CH_ZERO_CH0_BW_Breathing_3.wem
|
||||
Speech_CH_ZERO_CH0_BW_JR_save_1.wem
|
||||
Speech_CH_ZERO_CH0_BW_JR_save_2.wem
|
||||
Speech_CH_ZERO_CH0_CLIMB_THOUGHTS_1.wem
|
||||
Speech_CH_ZERO_CH0_CLIMB_THOUGHTS_2.wem
|
||||
Speech_CH_ZERO_CH0_ENCRYPTION_CARD1_1.wem
|
||||
Speech_CH_ZERO_CH0_ENCRYPTION_CARD3_1.wem
|
||||
Speech_CH_ZERO_CH0_ENCRYPTION_CARD3_2.wem
|
||||
Speech_CH_ZERO_CH0_fight_intro_3.wem
|
||||
Speech_CH_ZERO_CH0_FindCatwoman_1.wem
|
||||
Speech_CHAR_BATMAN_AfterCroc_1.wem
|
||||
Speech_CHAR_BATMAN_AfterCroc_2.wem
|
||||
Speech_CHAR_BATMAN_Batarang_1.wem
|
||||
Speech_CHAR_BATMAN_Cant_leave_mayor_1.wem
|
||||
Speech_CHAR_BATMAN_Cant_leave_ryder_1.wem
|
||||
Speech_CHAR_BATMAN_Cant_leave_ryder_2.wem
|
||||
Speech_CHAR_BATMAN_Cant_leave_taser_1.wem
|
||||
Speech_CHAR_BATMAN_Cant_leave_Vale_1.wem
|
||||
Speech_CHAR_BATMAN_Cops_1.wem
|
||||
Speech_CHAR_BATMAN_Cops_2.wem
|
||||
Speech_CHAR_BATMAN_Cops_3.wem
|
||||
Speech_CHAR_BATMAN_Cops_ice_1.wem
|
||||
Speech_CHAR_BATMAN_Cops_ice_2.wem
|
||||
Speech_CHAR_BATMAN_Crane_1.wem
|
||||
Speech_CHAR_BATMAN_Crane_2.wem
|
||||
Speech_CHAR_BATMAN_Crane_3.wem
|
||||
Speech_CHAR_BATMAN_Crane_4.wem
|
||||
Speech_CHAR_BATMAN_Crane_5.wem
|
||||
Speech_CHAR_BATMAN_Deadshot_Can_wait_1.wem
|
||||
Speech_CHAR_BATMAN_Destroy_last_crate_1.wem
|
||||
Speech_CHAR_BATMAN_Dont_leave_burlow_1.wem
|
||||
Speech_CHAR_BATMAN_Dont_leave_burlow_2.wem
|
||||
Speech_CHAR_BATMAN_Dont_leave_GCPD_1.wem
|
||||
Speech_CHAR_BATMAN_Dont_leave_GCPD_2.wem
|
||||
Speech_CHAR_BATMAN_Dont_use_gadget_ninja_1.wem
|
||||
Speech_CHAR_BATMAN_Dont_use_gadget_ninja_2.wem
|
||||
Speech_CHAR_BATMAN_Dont_use_gadget_ninja_3.wem
|
||||
Speech_CHAR_BATMAN_End_blood_trail_1.wem
|
||||
Speech_CHAR_BATMAN_Explosive_hostage_1.wem
|
||||
Speech_CHAR_BATMAN_Explosive_hostage_2.wem
|
||||
Speech_CHAR_BATMAN_Fail_mag_1.wem
|
||||
Speech_CHAR_BATMAN_Fail_mag_2.wem
|
||||
Speech_CHAR_BATMAN_Fail_Ninja_1.wem
|
||||
Speech_CHAR_BATMAN_Fail_Ninja_2.wem
|
||||
Speech_CHAR_BATMAN_Find_Jammers_first_1.wem
|
||||
Speech_CHAR_BATMAN_Find_Jammers_first_2.wem
|
||||
Speech_CHAR_BATMAN_Find_new_route_through_chimney_1.wem
|
||||
Speech_CHAR_BATMAN_Find_new_route_through_chimney_2.wem
|
||||
Speech_CHAR_BATMAN_Find_secret_door_1.wem
|
||||
Speech_CHAR_BATMAN_First_jamming_tower_1.wem
|
||||
Speech_CHAR_BATMAN_Freeze_jammer_fail_1.wem
|
||||
Speech_CHAR_BATMAN_Freeze_jammer_fail_2.wem
|
||||
Speech_CHAR_BATMAN_Freeze_jammer_not_used_1.wem
|
||||
Speech_CHAR_BATMAN_Frontend_taunt_1.wem
|
||||
Speech_CHAR_BATMAN_Frontend_taunt_2.wem
|
||||
Speech_CHAR_BATMAN_Frontend_taunt_3.wem
|
||||
Speech_CHAR_BATMAN_Frontend_taunt_4.wem
|
||||
Speech_CHAR_BATMAN_Frontend_taunt_5.wem
|
||||
Speech_CHAR_BATMAN_Frozen_air_1.wem
|
||||
Speech_CHAR_BATMAN_Frozen_air_2.wem
|
||||
Speech_CHAR_BATMAN_Gadget_Hostage_1.wem
|
||||
Speech_CHAR_BATMAN_GCPD_Safe_1.wem
|
||||
Speech_CHAR_BATMAN_Get_penguin_radio_1.wem
|
||||
Speech_CHAR_BATMAN_Get_penguin_radio_2.wem
|
||||
Speech_CHAR_BATMAN_Go_to_Subway_1.wem
|
||||
Speech_CHAR_BATMAN_Grapnel_Boost_1.wem
|
||||
Speech_CHAR_BATMAN_Hint_switch_1.wem
|
||||
Speech_CHAR_BATMAN_Hint_switch_2.wem
|
||||
Speech_CHAR_BATMAN_Hints_1.wem
|
||||
Speech_CHAR_BATMAN_Hints_2.wem
|
||||
Speech_CHAR_BATMAN_Hold_tight_1.wem
|
||||
Speech_CHAR_BATMAN_Hold_tight_2.wem
|
||||
Speech_CHAR_BATMAN_Hostages_Hostage_Taken_1.wem
|
||||
Speech_CHAR_BATMAN_Hostages_Hostage_Taken_2.wem
|
||||
Speech_CHAR_BATMAN_Hostages_quiet_1.wem
|
||||
Speech_CHAR_BATMAN_Hostages_quiet_2.wem
|
||||
Speech_CHAR_BATMAN_Inestigate_Crime_Scene_1.wem
|
||||
Speech_CHAR_BATMAN_Inestigate_Crime_Scene_2.wem
|
||||
Speech_CHAR_BATMAN_Inestigate_Crime_Scene_3.wem
|
||||
Speech_CHAR_BATMAN_Inestigate_Crime_Scene_4.wem
|
||||
Speech_CHAR_BATMAN_Jammed_frequencies_1.wem
|
||||
Speech_CHAR_BATMAN_Jammer_fail_deadshot_1.wem
|
||||
Speech_CHAR_BATMAN_Jammers_destroyed_1.wem
|
||||
Speech_CHAR_BATMAN_Jammers_destroyed_2.wem
|
||||
Speech_CHAR_BATMAN_Jammers_destroyed_3.wem
|
||||
Speech_CHAR_BATMAN_Jammers_destroyed_4.wem
|
||||
Speech_CHAR_BATMAN_Jammers_destroyed_5.wem
|
||||
Speech_CHAR_BATMAN_lookout_for_hush_victims_1.wem
|
||||
Speech_CHAR_BATMAN_Municipal_req_1.wem
|
||||
Speech_CHAR_BATMAN_Municipal_req_2.wem
|
||||
Speech_CHAR_BATMAN_MUS_no_Trail_1.wem
|
||||
Speech_CHAR_BATMAN_MUS_no_Trail_2.wem
|
||||
Speech_CHAR_BATMAN_Nav_Door_Locked_1.wem
|
||||
Speech_CHAR_BATMAN_Nav_Door_Locked_2.wem
|
||||
Speech_CHAR_BATMAN_Nav_Door_Locked_3.wem
|
||||
Speech_CHAR_BATMAN_Nav_Door_Locked_4.wem
|
||||
Speech_CHAR_BATMAN_No_entry_chimney_2.wem
|
||||
Speech_CHAR_BATMAN_No_entry_chimney_3.wem
|
||||
Speech_CHAR_BATMAN_No_entry_chimney_4.wem
|
||||
Speech_CHAR_BATMAN_Platform_grenade_1.wem
|
||||
Speech_CHAR_BATMAN_Riddle_1.wem
|
||||
Speech_CHAR_BATMAN_Rubble_1.wem
|
||||
Speech_CHAR_BATMAN_Safe_later_1.wem
|
||||
Speech_CHAR_BATMAN_Safe_later_2.wem
|
||||
Speech_CHAR_BATMAN_Save_doctor_in_steel_1.wem
|
||||
Speech_CHAR_BATMAN_Save_generic_1.wem
|
||||
Speech_CHAR_BATMAN_Save_generic_2.wem
|
||||
Speech_CHAR_BATMAN_Save_generic_3.wem
|
||||
Speech_CHAR_BATMAN_Save_generic_4.wem
|
||||
Speech_CHAR_BATMAN_Save_generic_5.wem
|
||||
Speech_CHAR_BATMAN_Save_generic_6.wem
|
||||
Speech_CHAR_BATMAN_Save_Talia_1.wem
|
||||
Speech_CHAR_BATMAN_Save_Talia_2.wem
|
||||
Speech_CHAR_BATMAN_Save_Talia_Gliding_1.wem
|
||||
Speech_CHAR_BATMAN_Save_vv_1.wem
|
||||
Speech_CHAR_BATMAN_Secure_Area_1.wem
|
||||
Speech_CHAR_BATMAN_Secure_Area_2.wem
|
||||
Speech_CHAR_BATMAN_Steam_1.wem
|
||||
Speech_CHAR_BATMAN_Steam_2.wem
|
||||
Speech_CHAR_BATMAN_TAUNT_1.wem
|
||||
Speech_CHAR_BATMAN_TAUNT_2.wem
|
||||
Speech_CHAR_BATMAN_TAUNT_3.wem
|
||||
Speech_CHAR_BATMAN_TAUNT_4.wem
|
||||
Speech_CHAR_BATMAN_TAUNT_5.wem
|
||||
Speech_CHAR_BATMAN_TAUNT_6.wem
|
||||
Speech_CHAR_BATMAN_TAUNT_7.wem
|
||||
Speech_CHAR_BATMAN_TAUNT_8.wem
|
||||
Speech_CHAR_BATMAN_TAUNT_9.wem
|
||||
Speech_CHAR_BATMAN_TAUNT_10.wem
|
||||
Speech_CHAR_BATMAN_Thawing_ice_1.wem
|
||||
Speech_CHAR_BATMAN_Tut_hostage_in_church1_1.wem
|
||||
Speech_CHAR_BATMAN_TYGER_Req_1.wem
|
||||
Speech_CHAR_BATMAN_UnderC2_Sword_1.wem
|
||||
Speech_CHAR_BATMAN_Water_too_hot_1.wem
|
||||
Speech_CHAR_BATMAN_Water_too_hot_2.wem
|
||||
Speech_CHAR_BATMAN_Zsasz_no_door_1.wem
|
||||
Speech_CHAR_BATMAN_Zsasz_no_door_2.wem
|
||||
Speech_CHAR_FREEZE_FreezeHeadOn_1.wem
|
||||
Speech_CHAR_FREEZE_FreezeRepeatTactic_1.wem
|
||||
Speech_CHAR_FREEZE_FreezeRepeatTactic_2.wem
|
||||
Speech_CHAR_FREEZE_FreezeSightedWarning_1.wem
|
||||
Speech_CHAR_ZSASZ_Batman_located_Zsasz_1.wem
|
||||
Speech_CHAR_ZSASZ_Batman_located_Zsasz_2.wem
|
||||
Speech_CHAR_ZSASZ_Batman_located_Zsasz_3.wem
|
||||
Speech_CHAR_ZSASZ_CALL1GLOAT_3.wem
|
||||
Speech_CHAR_ZSASZ_CALL1GLOAT_4.wem
|
||||
Speech_CHAR_ZSASZ_CALL2GLOAT_1.wem
|
||||
Speech_CHAR_ZSASZ_CALL8GLOAT_1.wem
|
||||
Speech_CHAR_ZSASZ_CALL8GLOAT_3.wem
|
||||
Speech_CHAR_ZSASZ_Find_me_first_1.wem
|
||||
Speech_CHAR_ZSASZ_Find_me_first_2.wem
|
||||
Speech_CHAR_ZSASZ_Find_me_first_3.wem
|
||||
Speech_CHAR_ZSASZ_Tease_2.wem
|
||||
Speech_CHURCH_B1_BM_LETMEIN_1.wem
|
||||
Speech_CHURCH_B1_Ch2_BM_ALF_Post_Gun_1.wem
|
||||
Speech_CHURCH_B1_Ch2_BM_ALF_Post_Gun_2.wem
|
||||
Speech_CHURCH_B1_CH2_BM_Cash_postrescue_4.wem
|
||||
Speech_CHURCH_B1_CH2_BM_Cash_postrescue_6.wem
|
||||
Speech_CHURCH_B1_CH2_BM_Cash_postrescue_7.wem
|
||||
Speech_CHURCH_B1_CH2_BM_Cash_postrescue_9.wem
|
||||
Speech_CHURCH_B1_CH2_BM_free_1.wem
|
||||
Speech_CHURCH_B1_CH2_BM_GOTO_TOWER_1.wem
|
||||
Speech_CHURCH_B1_CH2_BM_GOTO_TOWER_2.wem
|
||||
Speech_CHURCH_B1_CH2_BM_Sniper_1.wem
|
||||
Speech_CHURCH_B1_CH2_BM_Sniper_2.wem
|
||||
Speech_CHURCH_B1_Ch2_POST_Explosion_BM_Cash_1.wem
|
||||
Speech_CHURCH_B1_Ch2_POST_Explosion_BM_Cash_3.wem
|
||||
Speech_CHURCH_B1_CH2_post_G3toBM_1.wem
|
||||
Speech_CHURCH_B1_CH2_post_G3toBM_2.wem
|
||||
Speech_CHURCH_B1_CH2_post_MD2_comfort_FD4_BM_2.wem
|
||||
Speech_CHURCH_B1_CH2_post_MD3_tend_OC_BM_int_1.wem
|
||||
Speech_CHURCH_B1_Pred_tutorial_1.wem
|
||||
Speech_CHURCH_B1_Pred_tutorial_2.wem
|
||||
Speech_CHURCH_B1_Pred_tutorial_3.wem
|
||||
Speech_CHURCH_B1_Pred_tutorial_4.wem
|
||||
Speech_CHURCH_B1_Pred_tutorial_5.wem
|
||||
Speech_CHURCH_B1_Pred_tutorial_6.wem
|
||||
Speech_CHURCH_B1_Pred_tutorial_7.wem
|
||||
Speech_CHURCH_B1_Pred_tutorial_8.wem
|
||||
Speech_CHURCH_B1_RIDDLER_FIRST_RIDDLE_INTRO_3.wem
|
||||
Speech_CHURCH_B1_Riddler_guard_ENIGMA_2.wem
|
||||
Speech_CHURCH_B1_Riddler_guard_ENIGMA_4.wem
|
||||
Speech_CHURCH_B1_Riddlerguard_intro_2.wem
|
||||
Speech_CHURCH_B1_Riddlerguard_intro_4.wem
|
||||
Speech_CHURCH_B1_Riddlerguard_intro_6.wem
|
||||
Speech_CINEMATIC_CH1_CATWOMAN_SAVED_3.wem
|
||||
Speech_CINEMATIC_CH1_CATWOMAN_SAVED_5.wem
|
||||
Speech_CINEMATIC_CH1_CATWOMAN_SAVED_9.wem
|
||||
Speech_CINEMATIC_CH1_CATWOMAN_SAVED_22.wem
|
||||
Speech_CINEMATIC_CH1_CATWOMAN_SAVED_29.wem
|
||||
Speech_CINEMATIC_CH3_MEET_THE_PENGUIN_11.wem
|
||||
Speech_CINEMATIC_CH3_MEET_THE_PENGUIN_13.wem
|
||||
Speech_CINEMATIC_CH3_MEET_THE_PENGUIN_14.wem
|
||||
Speech_CINEMATIC_CH4_MEET_TALIA_1.wem
|
||||
Speech_CINEMATIC_CH4_MEET_TALIA_2.wem
|
||||
Speech_CINEMATIC_CH4_MEET_TALIA_6.wem
|
||||
Speech_CINEMATIC_CH4_MEET_TALIA_8.wem
|
||||
Speech_CINEMATIC_CH4_MEET_TALIA_13.wem
|
||||
Speech_CINEMATIC_CH4_MEET_TALIA_14.wem
|
||||
Speech_CINEMATIC_CH4_MEET_TALIA_17.wem
|
||||
Speech_CINEMATIC_CH4_MEET_TALIA_23.wem
|
||||
Speech_CINEMATIC_CH8_TALIA_KILLS_THE_WRONG_JOKER_5.wem
|
||||
Speech_CINEMATIC_CH8_TALIA_KILLS_THE_WRONG_JOKER_10.wem
|
||||
Speech_CINEMATIC_CH8_TALIA_KILLS_THE_WRONG_JOKER_13.wem
|
||||
Speech_CINEMATIC_CH8_TALIA_KILLS_THE_WRONG_JOKER_14.wem
|
||||
Speech_CINEMATIC_CH8_TALIA_KILLS_THE_WRONG_JOKER_15.wem
|
||||
Speech_CINEMATIC_CH8_TALIA_KILLS_THE_WRONG_JOKER_21.wem
|
||||
Speech_CINEMATIC_CH8_TALIA_KILLS_THE_WRONG_JOKER_31.wem
|
||||
Speech_CINEMATIC_CH8_TALIA_KILLS_THE_WRONG_JOKER_32.wem
|
||||
Speech_CINEMATIC_CH8_TALIA_KILLS_THE_WRONG_JOKER_35.wem
|
||||
Speech_CINEMATIC_SPECIAL_NEW_INTRO_2.wem
|
||||
Speech_CINEMATIC_SPECIAL_NEW_INTRO_4.wem
|
||||
Speech_CINEMATIC_SPECIAL_NEW_INTRO_6.wem
|
||||
Speech_CINEMATIC_SPECIAL_NEW_INTRO_13.wem
|
||||
Speech_CINEMATIC_SPECIAL_NEW_INTRO_14.wem
|
||||
Speech_CINEMATIC_SPECIAL_NEW_INTRO_17.wem
|
||||
Speech_CINEMATIC_SPECIAL_NEW_INTRO_24.wem
|
||||
Speech_COURT_B1_CH1_BM_Catwoman_1.wem
|
||||
Speech_COURT_B1_CH1_BM_Observeroom_1.wem
|
||||
Speech_COURT_B1_CH2_BM_Bullet_1.wem
|
||||
Speech_COURT_B1_CH2_BM_NotScanned_1.wem
|
||||
Speech_COURT_B1_CH2_BM_Scanned_1.wem
|
||||
Speech_COURT_B1_CH2_Bullet_floor_1.wem
|
||||
Speech_COURT_B1_CH2_Bullet_floor_2.wem
|
||||
Speech_COURT_B1_CH2_Bullet_window_1.wem
|
||||
Speech_COURT_B1_CH2_Bullet_window_2.wem
|
||||
Speech_COURT_B1_CH2_TF_Hang_conv_4.wem
|
||||
Speech_EMOTE_Batman_1.wem
|
||||
Speech_EMOTE_Batman_2.wem
|
||||
Speech_EMOTE_Batman_3.wem
|
||||
Speech_EMOTE_Batman_4.wem
|
||||
Speech_EMOTE_Batman_5.wem
|
||||
Speech_EMOTE_Batman_6.wem
|
||||
Speech_EMOTE_Batman_7.wem
|
||||
Speech_EMOTE_Batman_8.wem
|
||||
Speech_EMOTE_Batman_9.wem
|
||||
Speech_EMOTE_Batman_10.wem
|
||||
Speech_EMOTE_Batman_11.wem
|
||||
Speech_EMOTE_Batman_12.wem
|
||||
Speech_EMOTE_Batman_13.wem
|
||||
Speech_EMOTE_Batman_14.wem
|
||||
Speech_EMOTE_Batman_15.wem
|
||||
Speech_EMOTE_Batman_16.wem
|
||||
Speech_EMOTE_Batman_17.wem
|
||||
Speech_EMOTE_Batman_18.wem
|
||||
Speech_EMOTE_Batman_19.wem
|
||||
Speech_EMOTE_Batman_20.wem
|
||||
Speech_EMOTE_Batman_21.wem
|
||||
Speech_EMOTE_Batman_22.wem
|
||||
Speech_EMOTE_Batman_23.wem
|
||||
Speech_EMOTE_Batman_24.wem
|
||||
Speech_EMOTE_Batman_25.wem
|
||||
Speech_EMOTE_Batman_26.wem
|
||||
Speech_EMOTE_Batman_27.wem
|
||||
Speech_EMOTE_Batman_28.wem
|
||||
Speech_EMOTE_Batman_29.wem
|
||||
Speech_EMOTE_Batman_30.wem
|
||||
Speech_EMOTE_Batman_31.wem
|
||||
Speech_EMOTE_Batman_32.wem
|
||||
Speech_EMOTE_Batman_33.wem
|
||||
Speech_EMOTE_Batman_34.wem
|
||||
Speech_EMOTE_Batman_35.wem
|
||||
Speech_EMOTE_Batman_36.wem
|
||||
Speech_EMOTE_Batman_37.wem
|
||||
Speech_EMOTE_Batman_38.wem
|
||||
Speech_EMOTE_Batman_39.wem
|
||||
Speech_EMOTE_Batman_40.wem
|
||||
Speech_EMOTE_Batman_41.wem
|
||||
Speech_FORENSICS_Cold_1.wem
|
||||
Speech_FORENSICS_Cold_2.wem
|
||||
Speech_FORENSICS_Cold_3.wem
|
||||
Speech_FORENSICS_Cold_4.wem
|
||||
Speech_FORENSICS_Cold_5.wem
|
||||
Speech_GCPD_BM_Bombs_1.wem
|
||||
Speech_GCPD_BM_hackradio_thought_1.wem
|
||||
Speech_GCPD_BM_need_to_find_new_route_1.wem
|
||||
Speech_GCPD_Boss_intro_2.wem
|
||||
Speech_GCPD_CH3_BM_ALF_Codes_1.wem
|
||||
Speech_GCPD_CH3_BM_ALF_Codes_3.wem
|
||||
Speech_GCPD_CH3_BM_ALF_Codes_4.wem
|
||||
Speech_GCPD_Heartbeat_1.wem
|
||||
Speech_GCPD_Heartbeat_2.wem
|
||||
Speech_INTERROGATION_CH5_MAYOR_01_4.wem
|
||||
Speech_INTERROGATION_CH5_MAYOR_01_5.wem
|
||||
Speech_INTERROGATION_CH5_MAYOR_01_6.wem
|
||||
Speech_INTERROGATION_GCPD_A1_01_2.wem
|
||||
Speech_INTERROGATION_Hush_SS_01_1.wem
|
||||
Speech_INTERROGATION_Hush_SS_01_2.wem
|
||||
Speech_INTERROGATION_Hush_SS_01_6.wem
|
||||
Speech_INTERROGATION_Riddler_01_2.wem
|
||||
Speech_INTERROGATION_Riddler_01_3.wem
|
||||
Speech_INTERROGATION_Riddler_02_2.wem
|
||||
Speech_INTERROGATION_Riddler_02_3.wem
|
||||
Speech_INTERROGATION_Riddler_03_1.wem
|
||||
Speech_INTERROGATION_Riddler_03_3.wem
|
||||
Speech_INTERROGATION_Riddler_04_2.wem
|
||||
Speech_INTERROGATION_Riddler_04_3.wem
|
||||
Speech_INTERROGATION_Riddler_05_2.wem
|
||||
Speech_INTERROGATION_Riddler_05_3.wem
|
||||
Speech_INTERROGATION_Riddler_06_1.wem
|
||||
Speech_INTERROGATION_Riddler_07_1.wem
|
||||
Speech_INTERROGATION_Riddler_07_3.wem
|
||||
Speech_INTERROGATION_Riddler_08_2.wem
|
||||
Speech_INTERROGATION_Riddler_08_3.wem
|
||||
Speech_INTERROGATION_Riddler_09_1.wem
|
||||
Speech_INTERROGATION_Riddler_09_3.wem
|
||||
Speech_INTERROGATION_Riddler_10_1.wem
|
||||
Speech_INTERROGATION_Riddler_10_3.wem
|
||||
Speech_INTERROGATION_Riddler_11_2.wem
|
||||
Speech_INTERROGATION_Riddler_11_3.wem
|
||||
Speech_INTERROGATION_Riddler_12_1.wem
|
||||
Speech_INTERROGATION_Riddler_12_2.wem
|
||||
Speech_INTERROGATION_Riddler_13_1.wem
|
||||
Speech_INTERROGATION_Riddler_13_3.wem
|
||||
Speech_INTERROGATION_Riddler_14_2.wem
|
||||
Speech_INTERROGATION_Riddler_14_3.wem
|
||||
Speech_INTERROGATION_Riddler_15_2.wem
|
||||
Speech_INTERROGATION_Riddler_15_3.wem
|
||||
Speech_INTERROGATION_Riddler_16_1.wem
|
||||
Speech_INTERROGATION_Riddler_16_3.wem
|
||||
Speech_INTERROGATION_Riddler_17_2.wem
|
||||
Speech_INTERROGATION_Riddler_17_3.wem
|
||||
Speech_INTERROGATION_Riddler_18_1.wem
|
||||
Speech_INTERROGATION_Riddler_18_3.wem
|
||||
Speech_INTERROGATION_Riddler_19_2.wem
|
||||
Speech_INTERROGATION_Riddler_19_3.wem
|
||||
Speech_INTERROGATION_Riddler_20_1.wem
|
||||
Speech_INTERROGATION_Riddler_20_2.wem
|
||||
Speech_INTERROGATION_Riddler_21_2.wem
|
||||
Speech_INTERROGATION_Riddler_21_3.wem
|
||||
Speech_INTERROGATION_Riddler_22_1.wem
|
||||
Speech_INTERROGATION_Riddler_22_3.wem
|
||||
Speech_INTERROGATION_Riddler_23_1.wem
|
||||
Speech_INTERROGATION_Riddler_23_3.wem
|
||||
Speech_INTERROGATION_Riddler_24_1.wem
|
||||
Speech_INTERROGATION_Riddler_24_2.wem
|
||||
Speech_INTERROGATION_Riddler_25_2.wem
|
||||
Speech_INTERROGATION_Riddler_25_3.wem
|
||||
Speech_MAP_Final_map_1.wem
|
||||
Speech_MAP_Find_marker_1.wem
|
||||
Speech_MUSEUM_A1_CH3_Cop_C4_door_conv_1.wem
|
||||
Speech_MUSEUM_A1_CH3_Cop_C5_bridge_conv_2.wem
|
||||
Speech_MUSEUM_A1_CH3_Cop_C5_bridge_conv_4.wem
|
||||
Speech_MUSEUM_A1_CH3_Cop_C6_Patrol_conv_3.wem
|
||||
Speech_MUSEUM_A1_Ch3_dont_leave_cops_1.wem
|
||||
Speech_MUSEUM_A1_Ch3_dont_leave_cops_2.wem
|
||||
Speech_MUSEUM_A1_CH3_Find_suit_Early_1.wem
|
||||
Speech_MUSEUM_A1_Spot_thermal_1.wem
|
||||
Speech_MUSEUM_B1_CH3_BM_ALF_ORA_Catchup_1.wem
|
||||
Speech_MUSEUM_B1_CH3_BM_ALF_ORA_Catchup_5.wem
|
||||
Speech_MUSEUM_B1_CH3_BM_ALF_ORA_Catchup_7.wem
|
||||
Speech_MUSEUM_B1_CH3_BM_ALF_ORA_Catchup_8.wem
|
||||
Speech_MUSEUM_B2_CH3_BM_Ice_1.wem
|
||||
Speech_MUSEUM_B2_CH3_BM_Ice_2.wem
|
||||
Speech_MUSEUM_B2_CH3_BM_Ice_nobomb_1.wem
|
||||
Speech_MUSEUM_B2_CH3_Host2_Resc_1.wem
|
||||
Speech_MUSEUM_B2_CH3_Host2and3_Resc_BM_1.wem
|
||||
Speech_MUSEUM_B2_CH3_Host2and3_Resc_BM_2.wem
|
||||
Speech_MUSEUM_C1_CH3_BM_TalktoCop_1.wem
|
||||
Speech_MUSEUM_C1_CH3_Find_Jammers_outside_1.wem
|
||||
Speech_MUSEUM_C1_CH3_Museum_no_entry_1.wem
|
||||
Speech_MUSEUM_C1_CH3_Museum_no_entry_2.wem
|
||||
Speech_MUSEUM_C1_CH3_POST_THUG_COP_1.wem
|
||||
Speech_MUSEUM_C1_CH3_POST_THUG_COP_2.wem
|
||||
Speech_MUSEUM_C1_CH3_POST_THUG_COP_3.wem
|
||||
Speech_MUSEUM_C1_CH3_POST_THUG_COP_4.wem
|
||||
Speech_MUSEUM_C1_CH3_POST_THUG_COP_7.wem
|
||||
Speech_MUSEUM_C1_CH3_POST_THUG_COP_9.wem
|
||||
Speech_MUSEUM_C1_CH3_POST_THUG_COP_11.wem
|
||||
Speech_MUSEUM_C1_CH4_BM_ORACLE_NINJAS_4.wem
|
||||
Speech_MUSEUM_C1_CH4_BM_ORACLE_NINJAS_5.wem
|
||||
Speech_MUSEUM_C1_CH4_BM_ORACLE_NINJAS_6.wem
|
||||
Speech_MUSEUM_C1_CH4_Penguin_2.wem
|
||||
Speech_MUSEUM_C1_CH4_Post_Freeze_Chat_3.wem
|
||||
Speech_MUSEUM_C1_CH9_Peng_in_Cab_conv_2.wem
|
||||
Speech_MUSEUM_C1_CH9_Peng_in_Cab_conv_4.wem
|
||||
Speech_MUSEUM_C2_CH4_Freeze_revisit_2.wem
|
||||
Speech_MUSEUM_C2_CH4_Freeze_Revisit_Gun_2.wem
|
||||
Speech_MUSEUM_S1_CH3_BM_Find_Freeze_1.wem
|
||||
Speech_MUSEUM_S1_CH3_BM_Find_Freeze_2.wem
|
||||
Speech_MUSEUM_S1_CH3_BM_Find_Freeze_3.wem
|
||||
Speech_MUSEUM_S1_CH3_BM_Get_Jammer_1.wem
|
||||
Speech_MUSEUM_S1_CH3_BM_Jammer_1.wem
|
||||
Speech_MUSEUM_S1_CH3_BM_Jammer_2.wem
|
||||
Speech_MUSEUM_S1_CH3_BM_Jammer_Far_1.wem
|
||||
Speech_MUSEUM_S1_CH3_BM_Jammer_Far_2.wem
|
||||
Speech_MUSEUM_S1_CH3_BM_Jammer_Far_3.wem
|
||||
Speech_MUSEUM_S1_CH5_plus_post_door_chat_2.wem
|
||||
Speech_MUSEUM_S1_CH5_plus_post_door_chat_3.wem
|
||||
Speech_MUSEUM_S1_CH9_post_door_chat_2.wem
|
||||
Speech_MUSEUM_S1_CH9_post_door_chat_4.wem
|
||||
Speech_New_game_Quick_1.wem
|
||||
Speech_New_game_Quick_2.wem
|
||||
Speech_New_game_Quick_3.wem
|
||||
Speech_New_help_Enter_court_2.wem
|
||||
Speech_New_help_Spot_amored_1.wem
|
||||
Speech_New_help_Spot_jammer_1.wem
|
||||
Speech_New_help_Spot_minelayer_1.wem
|
||||
Speech_New_help_Spot_minelayer_2.wem
|
||||
Speech_New_help_Spot_shield_1.wem
|
||||
Speech_New_help_Spot_sniper_1.wem
|
||||
Speech_New_help_Spot_taser_1.wem
|
||||
Speech_RIDDLER_R1_Guard_Rescue_2.wem
|
||||
Speech_RIDDLER_R1_intro_1.wem
|
||||
Speech_RIDDLER_R1_intro_6.wem
|
||||
Speech_RIDDLER_R2_Saved_conv_1.wem
|
||||
Speech_RIDDLER_R2_Saved_conv_2.wem
|
||||
Speech_RIDDLER_R2_Saved_conv_5.wem
|
||||
Speech_RIDDLER_R3_new_BM_sit_tight_1.wem
|
||||
Speech_RIDDLER_R3_new_Hostage_rescue_1.wem
|
||||
Speech_RIDDLER_R3_new_Hostage_rescue_2.wem
|
||||
Speech_RIDDLER_R3_new_Hostage_rescue_3.wem
|
||||
Speech_RIDDLER_R4_Hostage_rescue_2.wem
|
||||
Speech_RIDDLER_R4_Hostage_rescue_3.wem
|
||||
Speech_RIDDLER_R4_Hostage_rescue_4.wem
|
||||
Speech_RIDDLER_R5_new_finished_bm_contact_Oracle_2.wem
|
||||
Speech_RIDDLER_R5_new_finished_bm_contact_Oracle_3.wem
|
||||
Speech_RIDDLER_R5_new_finished_bm_contact_Oracle_4.wem
|
||||
Speech_RIDDLER_R5_new_finished_oracle_give_location_2.wem
|
||||
Speech_RIDDLER_R5_new_finished_oracle_give_location_3.wem
|
||||
Speech_RIDDLER_R5_new_hostage_rescue_conv_1.wem
|
||||
Speech_RIDDLER_R5_new_hostage_rescue_conv_5.wem
|
||||
Speech_RIDDLER_R5_new_hostage_rescue_conv_6.wem
|
||||
Speech_RIDDLER_R6_BM_thoughts_about_vest_1.wem
|
||||
Speech_SEQUENCE_CATmeetBAT_2.wem
|
||||
Speech_SEQUENCE_CATmeetBAT_5.wem
|
||||
Speech_SEQUENCE_CATsaveBAT_3.wem
|
||||
Speech_SEQUENCE_CATsaveBAT_5.wem
|
||||
Speech_SEQUENCE_CH2_RESCUE_DOC_1.wem
|
||||
Speech_SEQUENCE_CH2_RESCUE_DOC_4.wem
|
||||
Speech_SEQUENCE_CH2_RESCUE_DOC_5.wem
|
||||
Speech_SEQUENCE_CH2_RESCUE_DOC_10.wem
|
||||
Speech_SEQUENCE_CH2_RESCUE_DOC_11.wem
|
||||
Speech_SEQUENCE_CH2_RESCUE_DOC_16.wem
|
||||
Speech_SEQUENCE_CH2_RESCUE_DOC_18.wem
|
||||
Speech_SEQUENCE_CH3_BM_WAKES_INTRO_2.wem
|
||||
Speech_SEQUENCE_CH3_BM_WAKES_INTRO_3.wem
|
||||
Speech_SEQUENCE_CH3_BM_WAKES_INTRO_4.wem
|
||||
Speech_SEQUENCE_CH3_BM_WAKES_INTRO_5.wem
|
||||
Speech_SEQUENCE_CH3_BM_WAKES_INTRO_7.wem
|
||||
Speech_SEQUENCE_CH3_MUSEUM_A1_BM_FREEZESUIT_1.wem
|
||||
Speech_SEQUENCE_CH3_MUSEUM_A1_BM_FREEZESUIT_2.wem
|
||||
Speech_SEQUENCE_CH3_MUSEUM_A1_GUARD_SAVED_1.wem
|
||||
Speech_SEQUENCE_CH3_MUSEUM_A1_GUARD_SAVED_3.wem
|
||||
Speech_SEQUENCE_CH3_MUSEUM_B2_CH3_HOST1_POST_RESCUE_1.wem
|
||||
Speech_SEQUENCE_CH3_MUSEUM_B2_CH3_HOST1_POST_RESCUE_4.wem
|
||||
Speech_SEQUENCE_CH3_MUSEUM_B2_CH3_HOST1_POST_RESCUE_5.wem
|
||||
Speech_SEQUENCE_CH3_MUSEUM_B2_CH3_HOST1_POST_RESCUE_6.wem
|
||||
Speech_SEQUENCE_CH4_MEET_ROBIN_1.wem
|
||||
Speech_SEQUENCE_CH4_MEET_ROBIN_2.wem
|
||||
Speech_SEQUENCE_CH4_MEET_ROBIN_6.wem
|
||||
Speech_SEQUENCE_CH4_MEET_ROBIN_10.wem
|
||||
Speech_SEQUENCE_CH4_MEET_ROBIN_13.wem
|
||||
Speech_SEQUENCE_CH4_MEET_ROBIN_14.wem
|
||||
Speech_SEQUENCE_CH4_MEET_ROBIN_18.wem
|
||||
Speech_SEQUENCE_CH4_MEET_ROBIN_19.wem
|
||||
Speech_SEQUENCE_CH4_UNDER_B1_TALIA_DRINK_4.wem
|
||||
Speech_SEQUENCE_CH4_UNDER_B1_TALIA_DRINK_6.wem
|
||||
Speech_SEQUENCE_CH4_UNDER_B1_TALIA_DRINK_8.wem
|
||||
Speech_SEQUENCE_RIDDLER_CAPTURE_2.wem
|
||||
Speech_SEQUENCE_RIDDLER_CAPTURE_3.wem
|
||||
Speech_SIDE_AZRAEL_Final_2.wem
|
||||
Speech_SIDE_AZRAEL_Final_5.wem
|
||||
Speech_SIDE_AZRAEL_Final_9.wem
|
||||
Speech_SIDE_BANE_All_Crates_1.wem
|
||||
Speech_SIDE_BANE_Bat_Symbol_1.wem
|
||||
Speech_SIDE_BANE_BM_comments_crate_before_Bane_1.wem
|
||||
Speech_SIDE_BANE_CAUGHT_CONV_2.wem
|
||||
Speech_SIDE_BANE_CAUGHT_CONV_3.wem
|
||||
Speech_SIDE_BANE_Crate_destroy_hint_1.wem
|
||||
Speech_SIDE_BANE_Crate_destroyed_hint_1.wem
|
||||
Speech_SIDE_BANE_END_4.wem
|
||||
Speech_SIDE_BANE_END_6.wem
|
||||
Speech_SIDE_BANE_END_8.wem
|
||||
Speech_SIDE_BANE_INTRO_ANSWERS_FIVE_1.wem
|
||||
Speech_SIDE_BANE_INTRO_ANSWERS_FOUR_1.wem
|
||||
Speech_SIDE_BANE_INTRO_ANSWERS_THREEANDLESS_1.wem
|
||||
Speech_SIDE_BANE_INTRO_POST_MUSEUM_2.wem
|
||||
Speech_SIDE_BANE_INTRO_POST_MUSEUM_5.wem
|
||||
Speech_SIDE_BANE_INTRO_POST_MUSEUM_6.wem
|
||||
Speech_SIDE_BANE_INTRO_POST_MUSEUM_8.wem
|
||||
Speech_SIDE_BANE_INTRO_POST_MUSEUM_10.wem
|
||||
Speech_SIDE_BANE_INTRO_PRE_MUSEUM_1.wem
|
||||
Speech_SIDE_BANE_INTRO_PRE_MUSEUM_2.wem
|
||||
Speech_SIDE_BANE_INTRO_PRE_MUSEUM_3.wem
|
||||
Speech_SIDE_BANE_INTRO_PRE_MUSEUM_7.wem
|
||||
Speech_SIDE_BANE_INTRO_PRE_MUSEUM_8.wem
|
||||
Speech_SIDE_BANE_Pre_tyger_conv_4.wem
|
||||
Speech_SIDE_BANE_STOP_2.wem
|
||||
Speech_SIDE_DEADSHOT_BM_Bullet_impacts_1.wem
|
||||
Speech_SIDE_DEADSHOT_BM_Bullet_impacts_2.wem
|
||||
Speech_SIDE_DEADSHOT_BM_Bullet_impacts_3.wem
|
||||
Speech_SIDE_DEADSHOT_BM_Crimescene_1.wem
|
||||
Speech_SIDE_DEADSHOT_BM_Crimescene_2.wem
|
||||
Speech_SIDE_DEADSHOT_BM_Crimescene_3.wem
|
||||
Speech_SIDE_DEADSHOT_BM_Crimescene_4.wem
|
||||
Speech_SIDE_DEADSHOT_BM_Crimescene_5.wem
|
||||
Speech_SIDE_DEADSHOT_BM_Crimescene_6.wem
|
||||
Speech_SIDE_DEADSHOT_BM_Crimescene_7.wem
|
||||
Speech_SIDE_DEADSHOT_BM_deadhot_schedule_1.wem
|
||||
Speech_SIDE_DEADSHOT_BM_deadhot_schedule_2.wem
|
||||
Speech_SIDE_DEADSHOT_BM_deadhot_schedule_3.wem
|
||||
Speech_SIDE_DEADSHOT_BM_deadhot_schedule_4.wem
|
||||
Speech_SIDE_DEADSHOT_BM_deadhot_schedule_5.wem
|
||||
Speech_SIDE_DEADSHOT_BM_deadhot_schedule_6.wem
|
||||
Speech_SIDE_DEADSHOT_BM_deadhot_schedule_7.wem
|
||||
Speech_SIDE_DEADSHOT_BM_deadhot_schedule_8.wem
|
||||
Speech_SIDE_DEADSHOT_BM_deadhot_schedule_9.wem
|
||||
Speech_SIDE_DEADSHOT_BM_FindStash_1.wem
|
||||
Speech_SIDE_DEADSHOT_BM_FindStash_2.wem
|
||||
Speech_SIDE_DEADSHOT_BM_Leaves_area_1.wem
|
||||
Speech_SIDE_DEADSHOT_BM_Leaves_area_2.wem
|
||||
Speech_SIDE_DEADSHOT_BM_No_prints_1.wem
|
||||
Speech_SIDE_DEADSHOT_BM_No_prints_2.wem
|
||||
Speech_SIDE_DEADSHOT_BM_No_prints_3.wem
|
||||
Speech_SIDE_DEADSHOT_BM_OR_recogniseshot2_2.wem
|
||||
Speech_SIDE_DEADSHOT_BM_OR_recogniseshot_1.wem
|
||||
Speech_SIDE_DEADSHOT_BM_OR_recogniseshot_4.wem
|
||||
Speech_SIDE_DEADSHOT_BM_Running_out_of_time_1.wem
|
||||
Speech_SIDE_DEADSHOT_BM_Running_out_of_time_2.wem
|
||||
Speech_SIDE_DEADSHOT_BM_Running_out_of_time_3.wem
|
||||
Speech_SIDE_DEADSHOT_BM_Running_out_of_time_4.wem
|
||||
Speech_SIDE_DEADSHOT_BM_Running_out_of_time_5.wem
|
||||
Speech_SIDE_DEADSHOT_BM_Search_area_for_evidence_1.wem
|
||||
Speech_SIDE_DEADSHOT_BM_Search_area_for_evidence_2.wem
|
||||
Speech_SIDE_DEADSHOT_BM_Search_area_for_evidence_3.wem
|
||||
Speech_SIDE_DEADSHOT_BM_See_sub_1.wem
|
||||
Speech_SIDE_DEADSHOT_BM_See_sub_2.wem
|
||||
Speech_SIDE_DEADSHOT_BM_See_sub_3.wem
|
||||
Speech_SIDE_DEADSHOT_BM_Substations_1.wem
|
||||
Speech_SIDE_DEADSHOT_BM_Substations_2.wem
|
||||
Speech_SIDE_DEADSHOT_BM_Substations_3.wem
|
||||
Speech_SIDE_DEADSHOT_END_seq_1.wem
|
||||
Speech_SIDE_DEADSHOT_RYDER_post_saved_1.wem
|
||||
Speech_SIDE_DEADSHOT_RYDER_post_saved_4.wem
|
||||
Speech_SIDE_DEADSHOT_Save_Ryder_3.wem
|
||||
Speech_SIDE_DEADSHOT_VIC1_BM_find_case_1.wem
|
||||
Speech_SIDE_DEADSHOT_VIC1_BM_OR_CONV_1.wem
|
||||
Speech_SIDE_DEADSHOT_VIC1_BM_OR_CONV_3.wem
|
||||
Speech_SIDE_DEADSHOT_VIC1_BM_OR_CONV_4.wem
|
||||
Speech_SIDE_DEADSHOT_VIC1_BM_OR_CONV_7.wem
|
||||
Speech_SIDE_DEADSHOT_VIC1_Conv_1.wem
|
||||
Speech_SIDE_DEADSHOT_VIC1_Conv_5.wem
|
||||
Speech_SIDE_DEADSHOT_VIC1_Conv_6.wem
|
||||
Speech_SIDE_DEADSHOT_VIC2_BM_Find_Tripod_1.wem
|
||||
Speech_SIDE_DEADSHOT_VIC2_BM_OR_CONV1_1.wem
|
||||
Speech_SIDE_DEADSHOT_VIC2_BM_OR_CONV1_3.wem
|
||||
Speech_SIDE_DEADSHOT_VIC2_BM_OR_CONV1_4.wem
|
||||
Speech_SIDE_DEADSHOT_VIC2_BM_OR_CONV2_1.wem
|
||||
Speech_SIDE_DEADSHOT_VIC2_BM_OR_CONV2_2.wem
|
||||
Speech_SIDE_DEADSHOT_VIC2_BM_OR_CONV2_5.wem
|
||||
Speech_SIDE_DEADSHOT_VIC2_Watertower_1.wem
|
||||
Speech_SIDE_DEADSHOT_VIC3_BM_comments_snow_1.wem
|
||||
Speech_SIDE_DEADSHOT_VIC3_BM_comments_snow_2.wem
|
||||
Speech_SIDE_DEADSHOT_VIC3_BM_OR_CONV1_1.wem
|
||||
Speech_SIDE_DEADSHOT_VIC3_BM_OR_CONV1_3.wem
|
||||
Speech_SIDE_DEADSHOT_VIC3_BM_OR_CONV1_4.wem
|
||||
Speech_SIDE_DEADSHOT_VIC3_BM_OR_CONV2_1.wem
|
||||
Speech_SIDE_DEADSHOT_VIC3_BM_OR_CONV2_2.wem
|
||||
Speech_SIDE_DEADSHOT_VIC3_BM_OR_CONV2_5.wem
|
||||
Speech_SIDE_DEADSHOT_VIC3_Ricochet_1.wem
|
||||
Speech_SIDE_HUSH_BM_post_hush_OR_3.wem
|
||||
Speech_SIDE_HUSH_BM_post_hush_OR_4.wem
|
||||
Speech_SIDE_HUSH_BM_post_hush_OR_5.wem
|
||||
Speech_SIDE_HUSH_BM_post_hush_OR_7.wem
|
||||
Speech_SIDE_HUSH_CHURCH_FMdoc_IDLE1_1.wem
|
||||
Speech_SIDE_HUSH_CHURCH_FMdoc_IDLE1_5.wem
|
||||
Speech_SIDE_HUSH_CHURCH_FMdoc_IDLE1_6.wem
|
||||
Speech_SIDE_HUSH_hobo_conv_1.wem
|
||||
Speech_SIDE_HUSH_hobo_conv_2.wem
|
||||
Speech_SIDE_HUSH_hobo_conv_4.wem
|
||||
Speech_SIDE_HUSH_Hush_speech_1.wem
|
||||
Speech_SIDE_HUSH_Hush_speech_4.wem
|
||||
Speech_SIDE_HUSH_Hush_speech_6.wem
|
||||
Speech_SIDE_HUSH_Hush_speech_7.wem
|
||||
Speech_SIDE_HUSH_Hush_speech_9.wem
|
||||
Speech_SIDE_HUSH_SEE_VIC1_1.wem
|
||||
Speech_SIDE_HUSH_SEE_VIC2_1.wem
|
||||
Speech_SIDE_HUSH_SEE_VIC3_1.wem
|
||||
Speech_SIDE_HUSH_VIC1_BOTH_SCANNED_1.wem
|
||||
Speech_SIDE_HUSH_VIC1_SCANBLOOD_1.wem
|
||||
Speech_SIDE_HUSH_VIC1_SCANFACE_1.wem
|
||||
Speech_SIDE_HUSH_VIC2_BOTH_SCANNED_3.wem
|
||||
Speech_SIDE_HUSH_VIC2_BOTH_SCANNED_4.wem
|
||||
Speech_SIDE_HUSH_VIC2_BOTH_SCANNED_POST_1.wem
|
||||
Speech_SIDE_HUSH_VIC2_BOTH_SCANNED_POST_2.wem
|
||||
Speech_SIDE_HUSH_VIC2_BOTH_SCANNED_POST_6.wem
|
||||
Speech_SIDE_HUSH_VIC2_SCANFACE_1.wem
|
||||
Speech_SIDE_HUSH_VIC2_SCANPRINT_1.wem
|
||||
Speech_SIDE_HUSH_VIC3_BOTHSCANNED_1.wem
|
||||
Speech_SIDE_HUSH_VIC3_SCANBLEACH_1.wem
|
||||
Speech_SIDE_HUSH_VIC3_SCANFACE_1.wem
|
||||
Speech_SIDE_MADHATTER_INTRO1_2.wem
|
||||
Speech_SIDE_MADHATTER_INTRO1_4.wem
|
||||
Speech_SIDE_MADHATTER_INTRO2_2.wem
|
||||
Speech_SIDE_MADHATTER_INTRO3_2.wem
|
||||
Speech_SIDE_MADHATTER_INTRO4_2.wem
|
||||
Speech_SIDE_MADHATTER_POST_FIGHT_2.wem
|
||||
Speech_SIDE_MADHATTER_POST_FIGHT_3.wem
|
||||
Speech_SIDE_NORA_BM_door_check_1.wem
|
||||
Speech_SIDE_STORY_Killer_Croc_1.wem
|
||||
Speech_SIDE_STORY_Nora_BM_Safe_1.wem
|
||||
Speech_SIDE_STORY_Nora_BM_Safe_2.wem
|
||||
Speech_SIDE_STORY_Nora_Freeze_BM_2.wem
|
||||
Speech_SIDE_STORY_Nora_Freeze_BM_3.wem
|
||||
Speech_SIDE_STORY_Nora_GCPD_Nora_1.wem
|
||||
Speech_SIDE_STORY_Nora_GCPD_Nora_3.wem
|
||||
Speech_SIDE_STORY_VV_pre_rescue_snipers_1.wem
|
||||
Speech_SIDE_STORY_VV_saved_conv_2.wem
|
||||
Speech_SIDE_STORY_VV_saved_conv_3.wem
|
||||
Speech_SIDE_STORY_VV_saved_conv_4.wem
|
||||
Speech_SIDE_STORY_VV_saved_conv_7.wem
|
||||
Speech_SIDE_STORY_VV_saved_conv_10.wem
|
||||
Speech_SIDE_STORY_Zsasz_BM_Save_hobo1_1.wem
|
||||
Speech_SIDE_STORY_Zsasz_BM_Save_hobo2_1.wem
|
||||
Speech_STEEL_A1_CH2_BM_Doc_Trap_1.wem
|
||||
Speech_STEEL_A1_CH2_BM_Doc_Trap_3.wem
|
||||
Speech_STEEL_A1_CH2_BM_Go_J_1.wem
|
||||
Speech_STEEL_A1_CH2_BM_Go_J_2.wem
|
||||
Speech_STEEL_A1_CH2_Rescue_early_1.wem
|
||||
Speech_STEEL_A1_CH2_Rescue_early_2.wem
|
||||
Speech_STEEL_B1_Ch2_BM_ALF_find_Control_3.wem
|
||||
Speech_STEEL_B1_Ch2_BM_ALF_find_Control_4.wem
|
||||
Speech_STEEL_B1_CH2_Goto_Doc_ALF_2.wem
|
||||
Speech_STEEL_B1_CH2_Goto_Doc_ALF_3.wem
|
||||
Speech_STEEL_B2_CH2_BM_find_MCR_1.wem
|
||||
Speech_STEEL_B2_CH2_BM_Observe_1.wem
|
||||
Speech_STEEL_B2_CH2_BM_Observe_2.wem
|
||||
Speech_STEEL_B2_CH2_BM_Observe_3.wem
|
||||
Speech_STEEL_B2_CH2_GoBack_LoadingBay_1.wem
|
||||
Speech_STEEL_C1_CH2_BM_Steam_1.wem
|
||||
Speech_STEEL_C1_CH2_BM_Steam_2.wem
|
||||
Speech_STEEL_C1_CH2_BM_UnstableFurnace_1.wem
|
||||
Speech_STEEL_C2_CH2_Batman_glass_1.wem
|
||||
Speech_STEEL_C2_CH2_BM_Ambush_1.wem
|
||||
Speech_STEEL_C2_CH2_check_door_1.wem
|
||||
Speech_STEEL_C2_CH2_check_door_hint_1.wem
|
||||
Speech_STEEL_C2_CH2_Smashglass_1.wem
|
||||
Speech_STEEL_C3_CH3_post_seq_BM_ALF_2.wem
|
||||
Speech_STEEL_C3_CH3_post_seq_BM_ALF_3.wem
|
||||
Speech_STEEL_C3_CH3_post_seq_BM_ALF_4.wem
|
||||
Speech_STEEL_C4_CH2_BM_Water_1.wem
|
||||
Speech_STEEL_C4_CH2_BM_Water_2.wem
|
||||
Speech_STEEL_C4_CH6_BM_Platform_1.wem
|
||||
Speech_STEEL_C4_CH6_BM_Platform_2.wem
|
||||
Speech_STEEL_C4_OR_INT_COUNCIL_1_2.wem
|
||||
Speech_STEEL_C4_OR_INT_COUNCIL_1_5.wem
|
||||
Speech_STEEL_C5_CH6_Doc_Sorry_1.wem
|
||||
Speech_STEEL_C5_CH6_Doc_Sorry_4.wem
|
||||
Speech_STEEL_C5_CH6_Doc_Sorry_6.wem
|
||||
Speech_STEEL_C5_CH6_Doc_Sorry_8.wem
|
||||
Speech_STEEL_C5_CH6_Doc_Sorry_9.wem
|
||||
Speech_STEEL_C5_OR_INT_COUNCIL_2_2.wem
|
||||
Speech_STREETS_A1_CH0_BM_ALF_3.wem
|
||||
Speech_STREETS_A1_CH0_BM_ALF_4.wem
|
||||
Speech_STREETS_A1_CH1_BM_CourtLocked_1.wem
|
||||
Speech_STREETS_A1_CH1_BM_Sneak_1.wem
|
||||
Speech_STREETS_A1_CH2_BM_ALF_Church_1.wem
|
||||
Speech_STREETS_A1_CH2_BM_ALF_Church_3.wem
|
||||
Speech_STREETS_A1_CH2_BM_Alf_Riddler_catchup_1.wem
|
||||
Speech_STREETS_A1_CH2_BM_Alf_Riddler_catchup_3.wem
|
||||
Speech_STREETS_A1_CH2_BM_Alf_Riddler_catchup_4.wem
|
||||
Speech_STREETS_A1_CH2_BM_ChurchSniper_1.wem
|
||||
Speech_STREETS_A1_CH2_BM_ChurchSniper_2.wem
|
||||
Speech_STREETS_A1_SZ2_CH1_JR_CONV_1.wem
|
||||
Speech_STREETS_A1_SZ2_CH1_JR_CONV_4.wem
|
||||
Speech_STREETS_A1_SZ2_CH1_JR_CONV_5.wem
|
||||
Speech_STREETS_A1_SZ2_CH1_JR_CONV_8.wem
|
||||
Speech_STREETS_A1_SZ4_CH1_S_Hobo_Bm_conv1_1.wem
|
||||
Speech_STREETS_A1_SZ4_CH1_S_Hobo_Bm_conv1_3.wem
|
||||
Speech_STREETS_A1_SZ4_CH1_S_Hobo_Bm_conv1_5.wem
|
||||
Speech_STREETS_A1_SZ4_CH1_S_Hobo_Bm_conv1_7.wem
|
||||
Speech_STREETS_A1_SZ4_CH1_S_Hobo_Bm_conv1_8.wem
|
||||
Speech_STREETS_A1_SZ12_CH1_S_HB4BM_Conv_2.wem
|
||||
Speech_STREETS_A1_SZ12_CH1_S_HB4BM_Conv_3.wem
|
||||
Speech_STREETS_A1_SZ12_CH1_S_HB4BM_Conv_4.wem
|
||||
Speech_STREETS_A2_CH2_BM_Defended_1.wem
|
||||
Speech_STREETS_A2_CH2_BM_Defended_2.wem
|
||||
Speech_STREETS_A3_CH2_BM_ALF_findroute_2.wem
|
||||
Speech_STREETS_A3_CH2_BM_ALF_findroute_4.wem
|
||||
Speech_STREETS_A3_CH2_BM_ALF_findroute_5.wem
|
||||
Speech_STREETS_A3_CH3_BM_Wake_Intro_2.wem
|
||||
Speech_STREETS_A3_CH3_BM_Wake_Intro_3.wem
|
||||
Speech_STREETS_A3_CH3_BM_Wake_Intro_4.wem
|
||||
Speech_STREETS_A3_CH7_BM_ALF_Tower_2.wem
|
||||
Speech_STREETS_A3_CH7_BM_ALF_Tower_3.wem
|
||||
Speech_STREETS_A3_CH7_NEW_DILEMMA_1.wem
|
||||
Speech_STREETS_A3_CH7_NEW_DILEMMA_3.wem
|
||||
Speech_STREETS_A3_CH7_NEW_DILEMMA_6.wem
|
||||
Speech_STREETS_A3_CH7_NEW_DILEMMA_7.wem
|
||||
Speech_STREETS_A3_CH7_NEW_DILEMMA_9.wem
|
||||
Speech_STREETS_A3_CollectGrapnelBoost_1.wem
|
||||
Speech_STREETS_A3_CollectGrapnelBoost_4.wem
|
||||
Speech_STREETS_A4_CH3_BM_ALF_restablish_comms_3.wem
|
||||
Speech_STREETS_A4_CH3_BM_ALF_restablish_comms_4.wem
|
||||
Speech_STREETS_A4_CH4_BM_Trail_underground_2.wem
|
||||
Speech_STREETS_A4_CH4_D4_post_thought_1.wem
|
||||
Speech_STREETS_A5_CH4_BM_ALF_UPDATE_1.wem
|
||||
Speech_STREETS_A5_CH4_BM_ALF_UPDATE_3.wem
|
||||
Speech_STREETS_A5_CH4_BM_Line_Launch_req_1.wem
|
||||
Speech_STREETS_A5_CH4_Need_tracker_1.wem
|
||||
Speech_STREETS_A5_CH4_Need_tracker_2.wem
|
||||
Speech_STREETS_A5_CH5_BM_OR_Mayor_2.wem
|
||||
Speech_STREETS_A5_CH5_BM_OR_Mayor_4.wem
|
||||
Speech_STREETS_A5_CH5_post_mayor_BM_ALF_2.wem
|
||||
Speech_STREETS_A5_CH5_post_mayor_BM_ALF_4.wem
|
||||
Speech_STREETS_A5_CH7_post_fight_conversation_1.wem
|
||||
Speech_STREETS_A5_CH7_post_fight_conversation_2.wem
|
||||
Speech_STREETS_A5_CH8_BM_OR_Catchup_1.wem
|
||||
Speech_STREETS_A5_CH8_BM_OR_Catchup_2.wem
|
||||
Speech_STREETS_A5_CH8_BM_OR_Catchup_3.wem
|
||||
Speech_STREETS_A5_CH8_BM_OR_Catchup_7.wem
|
||||
Speech_UNDER_A1_CH4_BM_close_doc_in_pred_BM_response_1.wem
|
||||
Speech_UNDER_A1_CH4_BM_close_doc_in_pred_BM_response_2.wem
|
||||
Speech_UNDER_A1_CH4_BM_close_doc_in_pred_BM_response_3.wem
|
||||
Speech_UNDER_A1_CH4_Doc_post_rescue_3.wem
|
||||
Speech_UNDER_A1_CH4_Doc_post_rescue_5.wem
|
||||
Speech_UNDER_A1_CH4_Doc_post_rescue_6.wem
|
||||
Speech_UNDER_A1_CH4_Doc_post_rescue_8.wem
|
||||
Speech_UNDER_A1_CH4_Post_BM_OR_3.wem
|
||||
Speech_UNDER_A1_CH4_Post_BM_OR_4.wem
|
||||
Speech_UNDER_A1_CH4_Post_BM_OR_5.wem
|
||||
Speech_UNDER_A1_CH4_Post_BM_OR_6.wem
|
||||
Speech_UNDER_A1_CH5_BM_NW_4.wem
|
||||
Speech_UNDER_A1_CH5_BM_NW_5.wem
|
||||
Speech_UNDER_A1_CH5_BM_NW_7.wem
|
||||
Speech_UNDER_A1_CH5_BM_OR_LAZARUS_1.wem
|
||||
Speech_UNDER_A1_CH5_BM_OR_LAZARUS_2.wem
|
||||
Speech_UNDER_A1_CH5_BM_OR_LAZARUS_5.wem
|
||||
Speech_UNDER_A1_CH5_BM_OR_LAZARUS_6.wem
|
||||
Speech_UNDER_A1_CH5_BM_OR_LAZARUS_7.wem
|
||||
Speech_UNDER_A1_CH7_Doc_Execute_2.wem
|
||||
Speech_UNDER_A1_CH7_Doc_Execute_5.wem
|
||||
Speech_UNDER_A1_CH7_Doc_Execute_6.wem
|
||||
Speech_UNDER_B1_CH4_TALIA_WALK_3.wem
|
||||
Speech_UNDER_B1_CH4_TALIA_WALK_6.wem
|
||||
Speech_UNDER_B4_CH7_Elevator_stuck_1.wem
|
||||
Speech_UNDER_B4_CH7_Elevator_stuck_2.wem
|
||||
Speech_UNDER_B4_CH7_ORACLE_near_top_3.wem
|
||||
Speech_UNDER_B4_CH7_ORACLE_near_top_4.wem
|
||||
Speech_UNDER_C1_CH4_BM_OR_Collapse1_1.wem
|
||||
Speech_UNDER_C1_CH4_BM_OR_stealguns_1.wem
|
||||
Speech_UNDER_C1_CH4_BM_OR_stealguns_3.wem
|
||||
Speech_UNDER_C1_Ch4_BM_Weapons_1.wem
|
||||
Speech_UNDER_C2_CH4_BM_DoorSealed_1.wem
|
||||
Speech_UNDER_C2_CH4_BM_DoorSealed_2.wem
|
||||
Speech_UNDER_C2_CH4_BM_DoorSealed_3.wem
|
||||
Speech_UNDER_C2_CH4_BM_DoorSealed_4.wem
|
||||
Speech_UNDER_C2_CH4_BM_DoorSealed_5.wem
|
||||
Speech_UNDER_C2_CH4_BM_watches_video_1.wem
|
||||
Speech_UNDER_C2_CH4_BM_watches_video_2.wem
|
||||
Speech_UNDER_C2_CH4_MG_corrupt_1.wem
|
||||
Speech_UNDER_C2_CH4_MG_corrupt_2.wem
|
||||
Speech_UNDER_S1_Batarang_1.wem
|
||||
Speech_UNDER_S1_Batman_sword_raised_1.wem
|
||||
Speech_UNDER_S1_Batman_sword_raised_2.wem
|
||||
Speech_UNDER_S1_Batman_sword_raised_3.wem
|
||||
Speech_UNDER_S1_Batman_sword_raised_4.wem
|
||||
Speech_UNDER_S1_Boss_conv1B_1.wem
|
||||
Speech_UNDER_S1_Sick_dream_post_1.wem
|
||||
Speech_UNDER_S1_Sick_dream_post_3.wem
|
||||
Speech_UNDER_S1_SwordStruggle1_2.wem
|
||||
Speech_UNDER_S1_Talia_TrialsDone_1.wem
|
||||
Speech_UNDER_S1_Talia_TrialsDone_2.wem
|
||||
Speech_Vertical_slice_JackRyderGDC_2.wem
|
||||
Speech_Vertical_slice_JackRyderGDC_7.wem
|
||||
Speech_Vertical_slice_JackRyderGDC_8.wem
|
||||
Speech_ZONES_STREET_A_ROAD3_CH1_BM_talk_to_hobo7_1.wem
|
||||
Speech_ZONES_STREET_A_ROAD3_CH1_BM_talk_to_hobo7_2.wem
|
||||
Speech_ZONES_STREET_A_ROAD3_CH1_BM_talk_to_hobo8_2.wem
|
||||
Speech_ZONES_STREET_A_ROAD3_CH1_BM_talk_to_hobo8_4.wem
|
||||
Speech_ZONES_STREET_A_ROAD3_CH3_BM_taak_to_HB8_1.wem
|
||||
Speech_ZONES_STREET_A_ROAD3_CH3_BM_taak_to_HB8_3.wem
|
||||
Speech_ZONES_STREET_A_ROAD3_CH3_BM_taak_to_HB8_4.wem
|
||||
Speech_ZONES_STREET_B_ROOF7_CH3_BM_talkto_hobo_1.wem
|
||||
Speech_ZONES_STREET_B_ROOF7_CH3_BM_talkto_hobo_2.wem
|
||||
Speech_ZONES_STREET_B_ROOF7_CH3_BM_talkto_hobo_5.wem
|
||||
Speech_ZONES_STREET_B_ROOF7_CH3_BM_talkto_hobo_7.wem
|
||||
Speech_ZP1_BM_ace_jump_1.wem
|
||||
119
data/catalog/source/ita/Calendar.txt
Normal file
119
data/catalog/source/ita/Calendar.txt
Normal file
@@ -0,0 +1,119 @@
|
||||
Speech_SIDE_CAT_MEET_CM_1.wem
|
||||
Speech_SIDE_CAT_MEET_CM_2.wem
|
||||
Speech_SIDE_CAT_MEET_CM_6.wem
|
||||
Speech_SIDE_STORY_April_1.wem
|
||||
Speech_SIDE_STORY_April_2.wem
|
||||
Speech_SIDE_STORY_Aug_1.wem
|
||||
Speech_SIDE_STORY_Aug_2.wem
|
||||
Speech_SIDE_STORY_CM_APR_1.wem
|
||||
Speech_SIDE_STORY_CM_APR_2.wem
|
||||
Speech_SIDE_STORY_CM_APR_3.wem
|
||||
Speech_SIDE_STORY_CM_APR_4.wem
|
||||
Speech_SIDE_STORY_CM_AUG_1.wem
|
||||
Speech_SIDE_STORY_CM_AUG_2.wem
|
||||
Speech_SIDE_STORY_CM_AUG_3.wem
|
||||
Speech_SIDE_STORY_CM_AUG_4.wem
|
||||
Speech_SIDE_STORY_CM_AUG_5.wem
|
||||
Speech_SIDE_STORY_CM_AUG_6.wem
|
||||
Speech_SIDE_STORY_CM_DEC_1.wem
|
||||
Speech_SIDE_STORY_CM_DEC_2.wem
|
||||
Speech_SIDE_STORY_CM_DEC_3.wem
|
||||
Speech_SIDE_STORY_CM_DEC_4.wem
|
||||
Speech_SIDE_STORY_CM_DEC_5.wem
|
||||
Speech_SIDE_STORY_CM_DEC_6.wem
|
||||
Speech_SIDE_STORY_CM_FEB_1.wem
|
||||
Speech_SIDE_STORY_CM_FEB_2.wem
|
||||
Speech_SIDE_STORY_CM_FEB_3.wem
|
||||
Speech_SIDE_STORY_CM_FEB_4.wem
|
||||
Speech_SIDE_STORY_CM_FEB_5.wem
|
||||
Speech_SIDE_STORY_CM_FEB_6.wem
|
||||
Speech_SIDE_STORY_CM_Intro_loop1_1.wem
|
||||
Speech_SIDE_STORY_CM_Intro_loop1_2.wem
|
||||
Speech_SIDE_STORY_CM_Intro_loop1_3.wem
|
||||
Speech_SIDE_STORY_CM_Intro_loop1_4.wem
|
||||
Speech_SIDE_STORY_CM_Intro_loop1_5.wem
|
||||
Speech_SIDE_STORY_CM_Intro_loop1_6.wem
|
||||
Speech_SIDE_STORY_CM_Intro_loop2_1.wem
|
||||
Speech_SIDE_STORY_CM_Intro_loop2_2.wem
|
||||
Speech_SIDE_STORY_CM_Intro_loop2_3.wem
|
||||
Speech_SIDE_STORY_CM_Intro_loop2_4.wem
|
||||
Speech_SIDE_STORY_CM_Intro_loop2_5.wem
|
||||
Speech_SIDE_STORY_CM_Intro_loop2_6.wem
|
||||
Speech_SIDE_STORY_CM_Intro_loop3_1.wem
|
||||
Speech_SIDE_STORY_CM_Intro_loop3_2.wem
|
||||
Speech_SIDE_STORY_CM_Intro_loop3_3.wem
|
||||
Speech_SIDE_STORY_CM_Intro_loop3_4.wem
|
||||
Speech_SIDE_STORY_CM_Intro_loop3_5.wem
|
||||
Speech_SIDE_STORY_CM_Intro_loop3_6.wem
|
||||
Speech_SIDE_STORY_CM_Intro_loop3_7.wem
|
||||
Speech_SIDE_STORY_CM_JAN_1.wem
|
||||
Speech_SIDE_STORY_CM_JAN_2.wem
|
||||
Speech_SIDE_STORY_CM_JAN_3.wem
|
||||
Speech_SIDE_STORY_CM_JAN_4.wem
|
||||
Speech_SIDE_STORY_CM_JAN_5.wem
|
||||
Speech_SIDE_STORY_CM_JUL_1.wem
|
||||
Speech_SIDE_STORY_CM_JUL_2.wem
|
||||
Speech_SIDE_STORY_CM_JUL_3.wem
|
||||
Speech_SIDE_STORY_CM_JUL_4.wem
|
||||
Speech_SIDE_STORY_CM_JUL_5.wem
|
||||
Speech_SIDE_STORY_CM_JUL_6.wem
|
||||
Speech_SIDE_STORY_CM_JUN_1.wem
|
||||
Speech_SIDE_STORY_CM_JUN_2.wem
|
||||
Speech_SIDE_STORY_CM_JUN_3.wem
|
||||
Speech_SIDE_STORY_CM_JUN_4.wem
|
||||
Speech_SIDE_STORY_CM_JUN_5.wem
|
||||
Speech_SIDE_STORY_CM_MAR_1.wem
|
||||
Speech_SIDE_STORY_CM_MAR_2.wem
|
||||
Speech_SIDE_STORY_CM_MAR_3.wem
|
||||
Speech_SIDE_STORY_CM_MAR_4.wem
|
||||
Speech_SIDE_STORY_CM_MAR_5.wem
|
||||
Speech_SIDE_STORY_CM_MAR_6.wem
|
||||
Speech_SIDE_STORY_CM_MAR_7.wem
|
||||
Speech_SIDE_STORY_CM_MAY_1.wem
|
||||
Speech_SIDE_STORY_CM_MAY_2.wem
|
||||
Speech_SIDE_STORY_CM_MAY_3.wem
|
||||
Speech_SIDE_STORY_CM_MAY_4.wem
|
||||
Speech_SIDE_STORY_CM_MAY_5.wem
|
||||
Speech_SIDE_STORY_CM_NOV_1.wem
|
||||
Speech_SIDE_STORY_CM_NOV_2.wem
|
||||
Speech_SIDE_STORY_CM_NOV_3.wem
|
||||
Speech_SIDE_STORY_CM_NOV_4.wem
|
||||
Speech_SIDE_STORY_CM_NOV_5.wem
|
||||
Speech_SIDE_STORY_CM_NOV_6.wem
|
||||
Speech_SIDE_STORY_CM_NOV_7.wem
|
||||
Speech_SIDE_STORY_CM_NOV_8.wem
|
||||
Speech_SIDE_STORY_CM_OCT_1.wem
|
||||
Speech_SIDE_STORY_CM_OCT_2.wem
|
||||
Speech_SIDE_STORY_CM_OCT_3.wem
|
||||
Speech_SIDE_STORY_CM_OCT_4.wem
|
||||
Speech_SIDE_STORY_CM_OCT_5.wem
|
||||
Speech_SIDE_STORY_CM_OCT_6.wem
|
||||
Speech_SIDE_STORY_CM_SEP_1.wem
|
||||
Speech_SIDE_STORY_CM_SEP_2.wem
|
||||
Speech_SIDE_STORY_CM_SEP_3.wem
|
||||
Speech_SIDE_STORY_CM_SEP_4.wem
|
||||
Speech_SIDE_STORY_CM_SEP_5.wem
|
||||
Speech_SIDE_STORY_CM_SEP_6.wem
|
||||
Speech_SIDE_STORY_CM_special_1.wem
|
||||
Speech_SIDE_STORY_CM_special_2.wem
|
||||
Speech_SIDE_STORY_CM_special_3.wem
|
||||
Speech_SIDE_STORY_Dec_1.wem
|
||||
Speech_SIDE_STORY_Dec_2.wem
|
||||
Speech_SIDE_STORY_Feb_1.wem
|
||||
Speech_SIDE_STORY_Feb_2.wem
|
||||
Speech_SIDE_STORY_Jan_1.wem
|
||||
Speech_SIDE_STORY_Jan_2.wem
|
||||
Speech_SIDE_STORY_July_1.wem
|
||||
Speech_SIDE_STORY_July_2.wem
|
||||
Speech_SIDE_STORY_June_1.wem
|
||||
Speech_SIDE_STORY_June_2.wem
|
||||
Speech_SIDE_STORY_March_1.wem
|
||||
Speech_SIDE_STORY_March_2.wem
|
||||
Speech_SIDE_STORY_May_1.wem
|
||||
Speech_SIDE_STORY_May_2.wem
|
||||
Speech_SIDE_STORY_Nov_1.wem
|
||||
Speech_SIDE_STORY_Nov_2.wem
|
||||
Speech_SIDE_STORY_October_1.wem
|
||||
Speech_SIDE_STORY_October_2.wem
|
||||
Speech_SIDE_STORY_Sept_1.wem
|
||||
Speech_SIDE_STORY_Sept_2.wem
|
||||
223
data/catalog/source/ita/Catwoman.txt
Normal file
223
data/catalog/source/ita/Catwoman.txt
Normal file
@@ -0,0 +1,223 @@
|
||||
Speech_CHAR_CATWOMAN_CW2_intro_mono_1.wem
|
||||
Speech_CHAR_CATWOMAN_CW2_intro_mono_2.wem
|
||||
Speech_CHAR_CATWOMAN_CW2_intro_mono_3.wem
|
||||
Speech_CHAR_CATWOMAN_CW2_intro_mono_4.wem
|
||||
Speech_CHAR_CATWOMAN_CW2_intro_mono_5.wem
|
||||
Speech_CHAR_CATWOMAN_CW2_intro_mono_6.wem
|
||||
Speech_CHAR_CATWOMAN_CW2_Round1_over_1.wem
|
||||
Speech_CHAR_CATWOMAN_CW2_Round1_over_2.wem
|
||||
Speech_CHAR_CATWOMAN_CW2_Round2_over_2.wem
|
||||
Speech_CHAR_CATWOMAN_CW2_Round2_over_3.wem
|
||||
Speech_CHAR_CATWOMAN_CW3_be_quick_1.wem
|
||||
Speech_CHAR_CATWOMAN_CW3_clear_room_first_1.wem
|
||||
Speech_CHAR_CATWOMAN_CW3_clear_room_first_2.wem
|
||||
Speech_CHAR_CATWOMAN_CW3_destroy_plant_intro_1.wem
|
||||
Speech_CHAR_CATWOMAN_CW3_destroy_plants_1.wem
|
||||
Speech_CHAR_CATWOMAN_CW3_destroy_plants_2.wem
|
||||
Speech_CHAR_CATWOMAN_CW3_dont_leave_goto_vault_1.wem
|
||||
Speech_CHAR_CATWOMAN_CW3_Getcards_1.wem
|
||||
Speech_CHAR_CATWOMAN_CW3_Getcards_2.wem
|
||||
Speech_CHAR_CATWOMAN_CW3_Goto_SecRoom_1.wem
|
||||
Speech_CHAR_CATWOMAN_CW3_Key_reminder_1.wem
|
||||
Speech_CHAR_CATWOMAN_CW3_Key_reminder_2.wem
|
||||
Speech_CHAR_CATWOMAN_CW3_remind_save_Bat_1.wem
|
||||
Speech_CHAR_CATWOMAN_CW3_remind_save_Bat_2.wem
|
||||
Speech_CHAR_CATWOMAN_CW3_remind_save_Bat_3.wem
|
||||
Speech_CHAR_CATWOMAN_CW3_save_Bat_1.wem
|
||||
Speech_CHAR_CATWOMAN_CW3_save_Bat_2.wem
|
||||
Speech_CHAR_CATWOMAN_CW3_vault_mono_1.wem
|
||||
Speech_CHAR_CATWOMAN_CW3_vault_mono_2.wem
|
||||
Speech_CHAR_CATWOMAN_CW3_vault_mono_3.wem
|
||||
Speech_CHAR_CATWOMAN_CW3_vault_mono_4.wem
|
||||
Speech_CHAR_CATWOMAN_CW_all_loot_1.wem
|
||||
Speech_CHAR_CATWOMAN_CW_all_loot_2.wem
|
||||
Speech_CHAR_CATWOMAN_CW_find_safe_1.wem
|
||||
Speech_CHAR_CATWOMAN_CW_find_safe_2.wem
|
||||
Speech_CHAR_CATWOMAN_CW_find_safe_3.wem
|
||||
Speech_CHAR_CATWOMAN_CW_FindIvy_1.wem
|
||||
Speech_CHAR_CATWOMAN_CW_Home_1.wem
|
||||
Speech_CHAR_CATWOMAN_CW_Home_2.wem
|
||||
Speech_CHAR_CATWOMAN_CW_see_vine_1.wem
|
||||
Speech_CHAR_CATWOMAN_CW_see_vine_2.wem
|
||||
Speech_CHAR_CATWOMAN_Fight_Taunts_Taunt_1.wem
|
||||
Speech_CHAR_CATWOMAN_Fight_Taunts_Taunt_2.wem
|
||||
Speech_CHAR_CATWOMAN_Fight_Taunts_Taunt_3.wem
|
||||
Speech_CHAR_CATWOMAN_Fight_Taunts_Taunt_4.wem
|
||||
Speech_CHAR_CATWOMAN_Fight_Taunts_Taunt_5.wem
|
||||
Speech_CHAR_CATWOMAN_Fight_Taunts_Taunt_6.wem
|
||||
Speech_CHAR_CATWOMAN_Fight_Taunts_Taunt_7.wem
|
||||
Speech_CHAR_CATWOMAN_Frontend_taunt_1.wem
|
||||
Speech_CHAR_CATWOMAN_Frontend_taunt_2.wem
|
||||
Speech_CHAR_CATWOMAN_Frontend_taunt_3.wem
|
||||
Speech_CHAR_CATWOMAN_Frontend_taunt_4.wem
|
||||
Speech_CHAR_CATWOMAN_Frontend_taunt_5.wem
|
||||
Speech_CHAR_CATWOMAN_Frontend_taunt_6.wem
|
||||
Speech_CHAR_CATWOMAN_Frontend_taunt_7.wem
|
||||
Speech_CHAR_CATWOMAN_Goto_Apart_1.wem
|
||||
Speech_CHAR_CATWOMAN_Goto_Apart_2.wem
|
||||
Speech_CHAR_CATWOMAN_Goto_Apart_3.wem
|
||||
Speech_CHAR_CATWOMAN_Grates_1.wem
|
||||
Speech_CHAR_CATWOMAN_Grates_2.wem
|
||||
Speech_CHAR_CATWOMAN_Grates_3.wem
|
||||
Speech_CHAR_CATWOMAN_New_plant_destroy_1.wem
|
||||
Speech_CHAR_CATWOMAN_New_plant_destroy_2.wem
|
||||
Speech_CINEMATIC_CH1_CATWOMAN_SAVED_1.wem
|
||||
Speech_CINEMATIC_CH1_CATWOMAN_SAVED_8.wem
|
||||
Speech_CINEMATIC_CH1_CATWOMAN_SAVED_11.wem
|
||||
Speech_CINEMATIC_CH1_CATWOMAN_SAVED_12.wem
|
||||
Speech_CINEMATIC_CH1_CATWOMAN_SAVED_13.wem
|
||||
Speech_CINEMATIC_CH1_CATWOMAN_SAVED_14.wem
|
||||
Speech_CINEMATIC_CH1_CATWOMAN_SAVED_15.wem
|
||||
Speech_CINEMATIC_CH1_CATWOMAN_SAVED_17.wem
|
||||
Speech_CINEMATIC_CH1_CATWOMAN_SAVED_18.wem
|
||||
Speech_CINEMATIC_CH1_CATWOMAN_SAVED_19.wem
|
||||
Speech_CINEMATIC_CH1_CATWOMAN_SAVED_20.wem
|
||||
Speech_CINEMATIC_CH1_CATWOMAN_SAVED_24.wem
|
||||
Speech_CINEMATIC_CH1_CATWOMAN_SAVED_25.wem
|
||||
Speech_CINEMATIC_CH1_CATWOMAN_SAVED_26.wem
|
||||
Speech_CINEMATIC_CH1_CATWOMAN_SAVED_27.wem
|
||||
Speech_CINEMATIC_CH1_CATWOMAN_SAVED_28.wem
|
||||
Speech_CINEMATIC_CH1_CATWOMAN_SAVED_31.wem
|
||||
Speech_COURT_B1_CH1_CW_Fight_idles_1.wem
|
||||
Speech_COURT_B1_CH1_CW_Fight_idles_2.wem
|
||||
Speech_COURT_B1_CH1_CW_Fight_idles_3.wem
|
||||
Speech_COURT_B1_CH1_CW_Fight_idles_4.wem
|
||||
Speech_COURT_B1_CH1_CW_rope_idles_1.wem
|
||||
Speech_COURT_B1_CH1_CW_rope_idles_2.wem
|
||||
Speech_COURT_B1_CH1_TwoFace_1_1.wem
|
||||
Speech_COURT_B1_CH1_TwoFace_2_4.wem
|
||||
Speech_EMOTE_CAT_1.wem
|
||||
Speech_EMOTE_CAT_2.wem
|
||||
Speech_EMOTE_CAT_3.wem
|
||||
Speech_EMOTE_CAT_4.wem
|
||||
Speech_EMOTE_CAT_5.wem
|
||||
Speech_EMOTE_CAT_6.wem
|
||||
Speech_EMOTE_CAT_7.wem
|
||||
Speech_EMOTE_CAT_8.wem
|
||||
Speech_EMOTE_CAT_9.wem
|
||||
Speech_EMOTE_CAT_10.wem
|
||||
Speech_EMOTE_CAT_11.wem
|
||||
Speech_EMOTE_CAT_12.wem
|
||||
Speech_EMOTE_CAT_13.wem
|
||||
Speech_EMOTE_CAT_14.wem
|
||||
Speech_EMOTE_CAT_15.wem
|
||||
Speech_EMOTE_CAT_16.wem
|
||||
Speech_EMOTE_CAT_17.wem
|
||||
Speech_EMOTE_CAT_18.wem
|
||||
Speech_EMOTE_CAT_19.wem
|
||||
Speech_EMOTE_CAT_20.wem
|
||||
Speech_EMOTE_CAT_21.wem
|
||||
Speech_EMOTE_CAT_22.wem
|
||||
Speech_EMOTE_CAT_23.wem
|
||||
Speech_EMOTE_CAT_24.wem
|
||||
Speech_EMOTE_CAT_25.wem
|
||||
Speech_EMOTE_CAT_26.wem
|
||||
Speech_EMOTE_CAT_27.wem
|
||||
Speech_EMOTE_CAT_28.wem
|
||||
Speech_EMOTE_CAT_29.wem
|
||||
Speech_EMOTE_CAT_30.wem
|
||||
Speech_EMOTE_CAT_31.wem
|
||||
Speech_EMOTE_CAT_32.wem
|
||||
Speech_EMOTE_CAT_33.wem
|
||||
Speech_EMOTE_CAT_34.wem
|
||||
Speech_EMOTE_CAT_35.wem
|
||||
Speech_EMOTE_CAT_36.wem
|
||||
Speech_EMOTE_CAT_37.wem
|
||||
Speech_EMOTE_CAT_38.wem
|
||||
Speech_EMOTE_CAT_39.wem
|
||||
Speech_EMOTE_CAT_40.wem
|
||||
Speech_EMOTE_CAT_41.wem
|
||||
Speech_EMOTE_CAT_42.wem
|
||||
Speech_EMOTE_CAT_43.wem
|
||||
Speech_EMOTE_CAT_44.wem
|
||||
Speech_EMOTE_CAT_45.wem
|
||||
Speech_EMOTE_CAT_46.wem
|
||||
Speech_EMOTE_CAT_47.wem
|
||||
Speech_EMOTE_CAT_48.wem
|
||||
Speech_EMOTE_CAT_49.wem
|
||||
Speech_EMOTE_CAT_50.wem
|
||||
Speech_EMOTE_CAT_51.wem
|
||||
Speech_EMOTE_CAT_52.wem
|
||||
Speech_EMOTE_CAT_53.wem
|
||||
Speech_EMOTE_CAT_54.wem
|
||||
Speech_EMOTE_CAT_55.wem
|
||||
Speech_EMOTE_CAT_56.wem
|
||||
Speech_EMOTE_CAT_57.wem
|
||||
Speech_EMOTE_CAT_58.wem
|
||||
Speech_EMOTE_CAT_59.wem
|
||||
Speech_INTERROGATION_Cat_01_2.wem
|
||||
Speech_INTERROGATION_Cat_01_3.wem
|
||||
Speech_INTERROGATION_Cat_01_3_1.wem
|
||||
Speech_SEQUENCE_CAT_GOTO_IVY_1.wem
|
||||
Speech_SEQUENCE_CAT_GOTO_IVY_2.wem
|
||||
Speech_SEQUENCE_CAT_GOTO_IVY_5.wem
|
||||
Speech_SEQUENCE_CAT_GOTO_IVY_6.wem
|
||||
Speech_SEQUENCE_CAT_GOTO_IVY_7.wem
|
||||
Speech_SEQUENCE_CAT_GOTO_IVY_9.wem
|
||||
Speech_SEQUENCE_CAT_IVY_1_1.wem
|
||||
Speech_SEQUENCE_CAT_IVY_1_3.wem
|
||||
Speech_SEQUENCE_CAT_IVY_2_1.wem
|
||||
Speech_SEQUENCE_CAT_IVY_2_3.wem
|
||||
Speech_SEQUENCE_CAT_IVY_2_7.wem
|
||||
Speech_SEQUENCE_CAT_IVY_2_8.wem
|
||||
Speech_SEQUENCE_CATmeetBAT_1.wem
|
||||
Speech_SEQUENCE_CATmeetBAT_3.wem
|
||||
Speech_SEQUENCE_CATmeetBAT_4.wem
|
||||
Speech_SEQUENCE_CATmeetBAT_6.wem
|
||||
Speech_SEQUENCE_CATmeetBAT_7.wem
|
||||
Speech_SEQUENCE_CATmeetBAT_8.wem
|
||||
Speech_SEQUENCE_CATmeetBAT_9.wem
|
||||
Speech_SEQUENCE_CATmeetBAT_10.wem
|
||||
Speech_SEQUENCE_CATsaveBAT_1.wem
|
||||
Speech_SEQUENCE_CATsaveBAT_2.wem
|
||||
Speech_SEQUENCE_CATsaveBAT_4.wem
|
||||
Speech_SEQUENCE_CATsaveBAT_6.wem
|
||||
Speech_SEQUENCE_CATsaveBAT_7.wem
|
||||
Speech_SEQUENCE_CH0_CATWOMAN_INTRO_1.wem
|
||||
Speech_SEQUENCE_CH0_CATWOMAN_INTRO_2.wem
|
||||
Speech_SEQUENCE_CH0_CATWOMAN_OUTRO_3.wem
|
||||
Speech_SEQUENCE_TWOFACE_END2_1.wem
|
||||
Speech_SEQUENCE_TWOFACE_END2_2.wem
|
||||
Speech_SEQUENCE_TWOFACE_END2_5.wem
|
||||
Speech_SEQUENCE_TWOFACE_END2_7.wem
|
||||
Speech_SEQUENCE_TWOFACE_END2_10.wem
|
||||
Speech_SEQUENCE_TWOFACE_END_1.wem
|
||||
Speech_SEQUENCE_TWOFACE_END_3.wem
|
||||
Speech_SEQUENCE_TWOFACE_END_4.wem
|
||||
Speech_SEQUENCE_TWOFACE_END_5.wem
|
||||
Speech_SIDE_CAT_CH6_cw_ivy_chat_3.wem
|
||||
Speech_SIDE_CAT_CH6_Dilemma_1.wem
|
||||
Speech_SIDE_CAT_CH6_Dilemma_2.wem
|
||||
Speech_SIDE_CAT_CH6_Dilemma_3.wem
|
||||
Speech_SIDE_CAT_CH6_Dilemma_4.wem
|
||||
Speech_SIDE_CAT_CH6_vault_1.wem
|
||||
Speech_SIDE_CAT_CH8b_ivy_chat_1.wem
|
||||
Speech_SIDE_CAT_CH8b_ivy_chat_2.wem
|
||||
Speech_SIDE_CAT_CH8b_ivy_chat_3.wem
|
||||
Speech_SIDE_CAT_CHURCH_LETMEIN_1.wem
|
||||
Speech_SIDE_CAT_CHURCH_LETMEIN_2.wem
|
||||
Speech_SIDE_CAT_Get_kit_at_IVY_1.wem
|
||||
Speech_SIDE_CAT_Get_kit_at_IVY_2.wem
|
||||
Speech_SIDE_CAT_Get_stuff_1.wem
|
||||
Speech_SIDE_CAT_Get_thugs_first_1.wem
|
||||
Speech_SIDE_CAT_MEET_BANE_1.wem
|
||||
Speech_SIDE_CAT_MEET_BANE_4.wem
|
||||
Speech_SIDE_CAT_MEET_CM_3.wem
|
||||
Speech_SIDE_CAT_MEET_CM_4.wem
|
||||
Speech_SIDE_CAT_MEET_CM_5.wem
|
||||
Speech_SIDE_CAT_MEET_Freeze_3.wem
|
||||
Speech_SIDE_CAT_MEET_Freeze_4.wem
|
||||
Speech_SIDE_CAT_MEET_Freeze_5.wem
|
||||
Speech_SIDE_CAT_MEET_Hatter_2.wem
|
||||
Speech_SIDE_CAT_MEET_Hatter_3.wem
|
||||
Speech_SIDE_CAT_MEET_PENGUIN_1.wem
|
||||
Speech_SIDE_CAT_MEET_PENGUIN_2.wem
|
||||
Speech_SIDE_CAT_MEET_PENGUIN_5.wem
|
||||
Speech_SIDE_CAT_MEET_PENGUIN_6.wem
|
||||
Speech_SIDE_CAT_Outside_lair_1.wem
|
||||
Speech_SIDE_CAT_Outside_lair_2.wem
|
||||
Speech_SIDE_CAT_RIDDLERS_games_1.wem
|
||||
Speech_SIDE_CAT_RIDDLERS_games_2.wem
|
||||
Speech_SIDE_CAT_Tunnel_collapse_1.wem
|
||||
Speech_SIDE_CAT_Underground_shake_1.wem
|
||||
Speech_SIDE_CAT_Zsasz_room_1.wem
|
||||
1
data/catalog/source/ita/Checkpoint.txt
Normal file
1
data/catalog/source/ita/Checkpoint.txt
Normal file
@@ -0,0 +1 @@
|
||||
10816
|
||||
3
data/catalog/source/ita/Croc.txt
Normal file
3
data/catalog/source/ita/Croc.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
Speech_SIDE_STORY_Killer_Croc_2.wem
|
||||
Speech_SIDE_STORY_Killer_Croc_4_1.wem
|
||||
Speech_SIDE_STORY_Killer_Croc_intro_1.wem
|
||||
34
data/catalog/source/ita/Deadshot.txt
Normal file
34
data/catalog/source/ita/Deadshot.txt
Normal file
@@ -0,0 +1,34 @@
|
||||
Speech_CHAR_DEADSHOT_BM_under_1.wem
|
||||
Speech_CHAR_DEADSHOT_BM_under_2.wem
|
||||
Speech_CHAR_DEADSHOT_BM_under_3.wem
|
||||
Speech_CHAR_DEADSHOT_Extra_cocky_1.wem
|
||||
Speech_CHAR_DEADSHOT_Extra_cocky_2.wem
|
||||
Speech_CHAR_DEADSHOT_Extra_cocky_3.wem
|
||||
Speech_CHAR_DEADSHOT_In_cover_1.wem
|
||||
Speech_CHAR_DEADSHOT_In_cover_2.wem
|
||||
Speech_CHAR_DEADSHOT_In_cover_3.wem
|
||||
Speech_CHAR_DEADSHOT_In_cover_4.wem
|
||||
Speech_CHAR_DEADSHOT_In_cover_5.wem
|
||||
Speech_CHAR_DEADSHOT_In_cover_6.wem
|
||||
Speech_CHAR_DEADSHOT_In_cover_7.wem
|
||||
Speech_CHAR_DEADSHOT_Shooting_idles_1.wem
|
||||
Speech_CHAR_DEADSHOT_Shooting_idles_2.wem
|
||||
Speech_CHAR_DEADSHOT_Shooting_idles_3.wem
|
||||
Speech_CHAR_DEADSHOT_Shooting_idles_4.wem
|
||||
Speech_CHAR_DEADSHOT_Shooting_idles_5.wem
|
||||
Speech_CHAR_DEADSHOT_Shooting_idles_6.wem
|
||||
Speech_CHAR_DEADSHOT_Shooting_idles_7.wem
|
||||
Speech_CHAR_DEADSHOT_Shooting_idles_8.wem
|
||||
Speech_GAME_OVER_Deadshot_1.wem
|
||||
Speech_GAME_OVER_Deadshot_2.wem
|
||||
Speech_GAME_OVER_Deadshot_3.wem
|
||||
Speech_SIDE_CAT_MEET_Deadshot_1.wem
|
||||
Speech_SIDE_CAT_MEET_Deadshot_2.wem
|
||||
Speech_SIDE_CAT_MEET_Deadshot_3.wem
|
||||
Speech_SIDE_DEADSHOT_Deadshot_angry_shout_1.wem
|
||||
Speech_SIDE_DEADSHOT_Deadshot_angry_shout_2.wem
|
||||
Speech_SIDE_DEADSHOT_Deadshot_angry_shout_retreat_1.wem
|
||||
Speech_SIDE_DEADSHOT_Deadshot_angry_shout_retreat_2.wem
|
||||
Speech_SIDE_DEADSHOT_DS_TO_BM_Post_capture_1.wem
|
||||
Speech_SIDE_DEADSHOT_DS_TO_BM_Post_capture_2.wem
|
||||
Speech_SIDE_DEADSHOT_END_seq_3.wem
|
||||
10817
data/catalog/source/ita/Files.txt
Normal file
10817
data/catalog/source/ita/Files.txt
Normal file
File diff suppressed because it is too large
Load Diff
238
data/catalog/source/ita/Freeze.txt
Normal file
238
data/catalog/source/ita/Freeze.txt
Normal file
@@ -0,0 +1,238 @@
|
||||
Speech_CHAR_FREEZE_BMsmashSculpt_1.wem
|
||||
Speech_CHAR_FREEZE_BMsmashSculpt_2.wem
|
||||
Speech_CHAR_FREEZE_BMsmashSculpt_3.wem
|
||||
Speech_CHAR_FREEZE_BMsmashSculpt_4.wem
|
||||
Speech_CHAR_FREEZE_BMsmashSculpt_5.wem
|
||||
Speech_CHAR_FREEZE_BMsmashSculpt_6.wem
|
||||
Speech_CHAR_FREEZE_BMsmashSculpt_7.wem
|
||||
Speech_CHAR_FREEZE_BMsmashSculpt_8.wem
|
||||
Speech_CHAR_FREEZE_Destroy_Generic_1.wem
|
||||
Speech_CHAR_FREEZE_Destroy_Generic_2.wem
|
||||
Speech_CHAR_FREEZE_Destroy_Generic_3.wem
|
||||
Speech_CHAR_FREEZE_Destroy_Generic_4.wem
|
||||
Speech_CHAR_FREEZE_Destroy_Generic_5.wem
|
||||
Speech_CHAR_FREEZE_Destroy_Generic_6.wem
|
||||
Speech_CHAR_FREEZE_Destroy_Generic_7.wem
|
||||
Speech_CHAR_FREEZE_Destroy_Generic_8.wem
|
||||
Speech_CHAR_FREEZE_Destroy_Generic_9.wem
|
||||
Speech_CHAR_FREEZE_Destroy_Generic_10.wem
|
||||
Speech_CHAR_FREEZE_DisableDetectiveMode_1.wem
|
||||
Speech_CHAR_FREEZE_DisableDetectiveMode_2.wem
|
||||
Speech_CHAR_FREEZE_DisableDetectiveMode_3.wem
|
||||
Speech_CHAR_FREEZE_DisableDetectiveMode_4.wem
|
||||
Speech_CHAR_FREEZE_DisableDetectiveMode_5.wem
|
||||
Speech_CHAR_FREEZE_DisableDetectiveMode_6.wem
|
||||
Speech_CHAR_FREEZE_DisableGlideKick_1.wem
|
||||
Speech_CHAR_FREEZE_DisableGlideKick_2.wem
|
||||
Speech_CHAR_FREEZE_DisableGlideKick_3.wem
|
||||
Speech_CHAR_FREEZE_DisableGlideKick_4.wem
|
||||
Speech_CHAR_FREEZE_FailedSilentTakedown_1.wem
|
||||
Speech_CHAR_FREEZE_FailedSilentTakedown_2.wem
|
||||
Speech_CHAR_FREEZE_FailedSilentTakedown_3.wem
|
||||
Speech_CHAR_FREEZE_FailedSilentTakedown_4.wem
|
||||
Speech_CHAR_FREEZE_FailedWeaponJamming_1.wem
|
||||
Speech_CHAR_FREEZE_FailedWeaponJamming_2.wem
|
||||
Speech_CHAR_FREEZE_FailedWeaponJamming_3.wem
|
||||
Speech_CHAR_FREEZE_FailedWeaponJamming_4.wem
|
||||
Speech_CHAR_FREEZE_FailedWeaponJamming_5.wem
|
||||
Speech_CHAR_FREEZE_FailedWeaponJamming_6.wem
|
||||
Speech_CHAR_FREEZE_FailedWeaponJamming_7.wem
|
||||
Speech_CHAR_FREEZE_FailedWeaponJamming_8.wem
|
||||
Speech_CHAR_FREEZE_FightIntro_1.wem
|
||||
Speech_CHAR_FREEZE_FightIntro_2.wem
|
||||
Speech_CHAR_FREEZE_FightIntro_3.wem
|
||||
Speech_CHAR_FREEZE_FightIntro_4.wem
|
||||
Speech_CHAR_FREEZE_FindFootprintTrail_1.wem
|
||||
Speech_CHAR_FREEZE_FindFootprintTrail_2.wem
|
||||
Speech_CHAR_FREEZE_FindFootprintTrail_3.wem
|
||||
Speech_CHAR_FREEZE_FindFootprintTrail_4.wem
|
||||
Speech_CHAR_FREEZE_FindFootprintTrail_5.wem
|
||||
Speech_CHAR_FREEZE_FindFootprintTrail_6.wem
|
||||
Speech_CHAR_FREEZE_FindFootprintTrail_7.wem
|
||||
Speech_CHAR_FREEZE_FindFootprintTrail_8.wem
|
||||
Speech_CHAR_FREEZE_FireFreezeBeam_1.wem
|
||||
Speech_CHAR_FREEZE_FireFreezeBeam_2.wem
|
||||
Speech_CHAR_FREEZE_FireFreezeBeam_3.wem
|
||||
Speech_CHAR_FREEZE_FireFreezeBeam_4.wem
|
||||
Speech_CHAR_FREEZE_FireFreezeBeam_5.wem
|
||||
Speech_CHAR_FREEZE_FireFreezeBeam_6.wem
|
||||
Speech_CHAR_FREEZE_FireGrenadeBlind_1.wem
|
||||
Speech_CHAR_FREEZE_FireGrenadeBlind_2.wem
|
||||
Speech_CHAR_FREEZE_FireGrenadeBlind_3.wem
|
||||
Speech_CHAR_FREEZE_FireGrenadeBlind_4.wem
|
||||
Speech_CHAR_FREEZE_FireGrenadeBlind_5.wem
|
||||
Speech_CHAR_FREEZE_FireGrenadeBlind_6.wem
|
||||
Speech_CHAR_FREEZE_FireGrenadeBlind_7.wem
|
||||
Speech_CHAR_FREEZE_FireGrenadeBlind_8.wem
|
||||
Speech_CHAR_FREEZE_FireGrenadeDirect_1.wem
|
||||
Speech_CHAR_FREEZE_FireGrenadeDirect_2.wem
|
||||
Speech_CHAR_FREEZE_FireGrenadeDirect_3.wem
|
||||
Speech_CHAR_FREEZE_FireGrenadeDirect_4.wem
|
||||
Speech_CHAR_FREEZE_FireSeekers_1.wem
|
||||
Speech_CHAR_FREEZE_FireSeekers_2.wem
|
||||
Speech_CHAR_FREEZE_FireSeekers_3.wem
|
||||
Speech_CHAR_FREEZE_FireSeekers_4.wem
|
||||
Speech_CHAR_FREEZE_FreezeSmashedSculpture_1.wem
|
||||
Speech_CHAR_FREEZE_FreezeSmashedSculpture_2.wem
|
||||
Speech_CHAR_FREEZE_HearBM_1.wem
|
||||
Speech_CHAR_FREEZE_HearBM_2.wem
|
||||
Speech_CHAR_FREEZE_HearBM_3.wem
|
||||
Speech_CHAR_FREEZE_HearBM_4.wem
|
||||
Speech_CHAR_FREEZE_HearBM_5.wem
|
||||
Speech_CHAR_FREEZE_HearBM_6.wem
|
||||
Speech_CHAR_FREEZE_HearBM_7.wem
|
||||
Speech_CHAR_FREEZE_HearBM_8.wem
|
||||
Speech_CHAR_FREEZE_HearLineLauncher_1.wem
|
||||
Speech_CHAR_FREEZE_HearLineLauncher_2.wem
|
||||
Speech_CHAR_FREEZE_HearLineLauncher_3.wem
|
||||
Speech_CHAR_FREEZE_HearLineLauncher_4.wem
|
||||
Speech_CHAR_FREEZE_HearLineLauncher_5.wem
|
||||
Speech_CHAR_FREEZE_HearLineLauncher_6.wem
|
||||
Speech_CHAR_FREEZE_NeutraliseWindow_1.wem
|
||||
Speech_CHAR_FREEZE_NeutraliseWindow_2.wem
|
||||
Speech_CHAR_FREEZE_NeutraliseWindow_3.wem
|
||||
Speech_CHAR_FREEZE_NeutraliseWindow_4.wem
|
||||
Speech_CHAR_FREEZE_Neutralize_Generic_1.wem
|
||||
Speech_CHAR_FREEZE_Neutralize_Generic_2.wem
|
||||
Speech_CHAR_FREEZE_Neutralize_Generic_3.wem
|
||||
Speech_CHAR_FREEZE_Neutralize_Generic_4.wem
|
||||
Speech_CHAR_FREEZE_Neutralize_Generic_5.wem
|
||||
Speech_CHAR_FREEZE_Neutralize_Generic_6.wem
|
||||
Speech_CHAR_FREEZE_Neutralize_Generic_7.wem
|
||||
Speech_CHAR_FREEZE_Neutralize_Generic_8.wem
|
||||
Speech_CHAR_FREEZE_Neutralize_Generic_9.wem
|
||||
Speech_CHAR_FREEZE_Neutralize_Generic_10.wem
|
||||
Speech_CHAR_FREEZE_Neutralize_Generic_11.wem
|
||||
Speech_CHAR_FREEZE_Neutralize_Generic_12.wem
|
||||
Speech_CHAR_FREEZE_Neutralize_Generic_13.wem
|
||||
Speech_CHAR_FREEZE_Neutralize_Generic_14.wem
|
||||
Speech_CHAR_FREEZE_Neutralize_Generic_15.wem
|
||||
Speech_CHAR_FREEZE_Neutralize_Generic_16.wem
|
||||
Speech_CHAR_FREEZE_NeverAgainTauntGeneric_1.wem
|
||||
Speech_CHAR_FREEZE_NeverAgainTauntGeneric_2.wem
|
||||
Speech_CHAR_FREEZE_NeverAgainTauntGeneric_3.wem
|
||||
Speech_CHAR_FREEZE_NeverAgainTauntGeneric_4.wem
|
||||
Speech_CHAR_FREEZE_NeverAgainTauntGeneric_5.wem
|
||||
Speech_CHAR_FREEZE_NeverAgainTauntGeneric_6.wem
|
||||
Speech_CHAR_FREEZE_NeverAgainTauntLineLauncher_1.wem
|
||||
Speech_CHAR_FREEZE_NeverAgainTauntLineLauncher_2.wem
|
||||
Speech_CHAR_FREEZE_NeverAgainTauntLineLauncher_3.wem
|
||||
Speech_CHAR_FREEZE_NeverAgainTauntLineLauncher_4.wem
|
||||
Speech_CHAR_FREEZE_NeverAgainTauntLineLauncher_5.wem
|
||||
Speech_CHAR_FREEZE_NeverAgainTauntLineLauncher_6.wem
|
||||
Speech_CHAR_FREEZE_NeverAgainTauntLineLauncher_7.wem
|
||||
Speech_CHAR_FREEZE_NeverAgainTauntLineLauncher_8.wem
|
||||
Speech_CHAR_FREEZE_NeverAgainTauntMagblast_1.wem
|
||||
Speech_CHAR_FREEZE_NeverAgainTauntMagblast_2.wem
|
||||
Speech_CHAR_FREEZE_NeverAgainTauntMagblast_3.wem
|
||||
Speech_CHAR_FREEZE_NeverAgainTauntMagblast_4.wem
|
||||
Speech_CHAR_FREEZE_NeverAgainTauntSilentTD_1.wem
|
||||
Speech_CHAR_FREEZE_NeverAgainTauntSilentTD_2.wem
|
||||
Speech_CHAR_FREEZE_NeverAgainTauntSilentTD_3.wem
|
||||
Speech_CHAR_FREEZE_NeverAgainTauntSilentTD_4.wem
|
||||
Speech_CHAR_FREEZE_NoticedWalkingInCircles_1.wem
|
||||
Speech_CHAR_FREEZE_NoticedWalkingInCircles_2.wem
|
||||
Speech_CHAR_FREEZE_ReachedEndOfTrail_1.wem
|
||||
Speech_CHAR_FREEZE_ReachedEndOfTrail_2.wem
|
||||
Speech_CHAR_FREEZE_ReachedEndOfTrail_3.wem
|
||||
Speech_CHAR_FREEZE_ReachedEndOfTrail_4.wem
|
||||
Speech_CHAR_FREEZE_RecogniseSound_1.wem
|
||||
Speech_CHAR_FREEZE_RecogniseSound_2.wem
|
||||
Speech_CHAR_FREEZE_RecogniseSound_3.wem
|
||||
Speech_CHAR_FREEZE_RecogniseSound_4.wem
|
||||
Speech_CHAR_FREEZE_SeekerDestroyed_1.wem
|
||||
Speech_CHAR_FREEZE_SeekerDestroyed_2.wem
|
||||
Speech_CHAR_FREEZE_SeekerDestroyed_3.wem
|
||||
Speech_CHAR_FREEZE_SeekerDestroyed_4.wem
|
||||
Speech_CHAR_FREEZE_SeekerDestroyed_5.wem
|
||||
Speech_CHAR_FREEZE_SeekerDestroyed_6.wem
|
||||
Speech_CHAR_FREEZE_SeekerDestroyed_7.wem
|
||||
Speech_CHAR_FREEZE_SeekerDestroyed_8.wem
|
||||
Speech_CHAR_FREEZE_SeekerFindsBatman_1.wem
|
||||
Speech_CHAR_FREEZE_SeekerFindsBatman_2.wem
|
||||
Speech_CHAR_FREEZE_SeekerFindsBatman_3.wem
|
||||
Speech_CHAR_FREEZE_SeekerFindsBatman_4.wem
|
||||
Speech_CHAR_FREEZE_SeekerFindsBatman_5.wem
|
||||
Speech_CHAR_FREEZE_SeekerFindsBatman_6.wem
|
||||
Speech_CHAR_FREEZE_SeekerFindsBatman_7.wem
|
||||
Speech_CHAR_FREEZE_SeekerFindsBatman_8.wem
|
||||
Speech_CHAR_FREEZE_SpotBM_1.wem
|
||||
Speech_CHAR_FREEZE_SpotBM_2.wem
|
||||
Speech_CHAR_FREEZE_SpotBM_3.wem
|
||||
Speech_CHAR_FREEZE_SpotBM_4.wem
|
||||
Speech_CHAR_FREEZE_SpotBM_5.wem
|
||||
Speech_CHAR_FREEZE_SpotBM_6.wem
|
||||
Speech_CHAR_FREEZE_SpotBM_7.wem
|
||||
Speech_CHAR_FREEZE_SpotBM_8.wem
|
||||
Speech_CHAR_FREEZE_SpotBM_9.wem
|
||||
Speech_CHAR_FREEZE_SpotBM_10.wem
|
||||
Speech_CHAR_FREEZE_SpotBM_11.wem
|
||||
Speech_CHAR_FREEZE_SpotBM_12.wem
|
||||
Speech_CHAR_FREEZE_TakeDownReaction_1.wem
|
||||
Speech_CHAR_FREEZE_TakeDownReaction_2.wem
|
||||
Speech_CHAR_FREEZE_TakeDownReaction_3.wem
|
||||
Speech_CHAR_FREEZE_TakeDownReaction_4.wem
|
||||
Speech_CHAR_FREEZE_TakeDownReaction_5.wem
|
||||
Speech_CHAR_FREEZE_TakeDownReaction_6.wem
|
||||
Speech_CHAR_FREEZE_TakeDownReaction_7.wem
|
||||
Speech_CHAR_FREEZE_TakeDownReaction_8.wem
|
||||
Speech_CHAR_FREEZE_TakeDownReaction_9.wem
|
||||
Speech_CHAR_FREEZE_TakeDownReaction_10.wem
|
||||
Speech_CHAR_FREEZE_TakeDownReaction_11.wem
|
||||
Speech_CHAR_FREEZE_TakeDownReaction_12.wem
|
||||
Speech_CHAR_FREEZE_TakeDownReaction_13.wem
|
||||
Speech_CHAR_FREEZE_TakeDownReaction_14.wem
|
||||
Speech_CHAR_FREEZE_TakeDownReaction_15.wem
|
||||
Speech_CHAR_FREEZE_TakeDownReaction_16.wem
|
||||
Speech_CHAR_FREEZE_TauntWithoutVisual_1.wem
|
||||
Speech_CHAR_FREEZE_TauntWithoutVisual_2.wem
|
||||
Speech_CHAR_FREEZE_TauntWithoutVisual_3.wem
|
||||
Speech_CHAR_FREEZE_TauntWithoutVisual_4.wem
|
||||
Speech_CHAR_FREEZE_TauntWithoutVisual_5.wem
|
||||
Speech_CHAR_FREEZE_TauntWithoutVisual_6.wem
|
||||
Speech_CHAR_FREEZE_TauntWithoutVisual_7.wem
|
||||
Speech_CHAR_FREEZE_TauntWithoutVisual_8.wem
|
||||
Speech_CHAR_FREEZE_TauntWithVisual_1.wem
|
||||
Speech_CHAR_FREEZE_TauntWithVisual_2.wem
|
||||
Speech_CHAR_FREEZE_TauntWithVisual_3.wem
|
||||
Speech_CHAR_FREEZE_TauntWithVisual_4.wem
|
||||
Speech_CHAR_FREEZE_TauntWithVisual_5.wem
|
||||
Speech_CHAR_FREEZE_TauntWithVisual_6.wem
|
||||
Speech_CHAR_FREEZE_TauntWithVisual_7.wem
|
||||
Speech_CHAR_FREEZE_TauntWithVisual_8.wem
|
||||
Speech_CHAR_FREEZE_WeaponJammingReaction_1.wem
|
||||
Speech_CHAR_FREEZE_WeaponJammingReaction_2.wem
|
||||
Speech_GAME_OVER_Mr_Freeze_1.wem
|
||||
Speech_GAME_OVER_Mr_Freeze_2.wem
|
||||
Speech_GCPD_Freeze_working_1.wem
|
||||
Speech_GCPD_Freeze_working_2.wem
|
||||
Speech_GCPD_Freeze_working_3.wem
|
||||
Speech_GCPD_Freeze_working_4.wem
|
||||
Speech_MAP_Freeze_Nora_Map_1.wem
|
||||
Speech_MUSEUM_C1_CH4_Post_Freeze_Chat_1.wem
|
||||
Speech_MUSEUM_C1_CH4_Post_Freeze_Chat_2.wem
|
||||
Speech_MUSEUM_C1_CH4_Post_Freeze_idle_1.wem
|
||||
Speech_MUSEUM_C1_CH4_Post_Freeze_idle_2.wem
|
||||
Speech_MUSEUM_C2_CH4_Find_suit_idles_1.wem
|
||||
Speech_MUSEUM_C2_CH4_Find_suit_idles_2.wem
|
||||
Speech_MUSEUM_C2_CH4_Freeze_revisit_1.wem
|
||||
Speech_MUSEUM_C2_CH4_Freeze_Revisit_Gun_1.wem
|
||||
Speech_MUSEUM_C2_CH4_Freeze_Revisit_Gun_3.wem
|
||||
Speech_MUSEUM_C2_CH4_Freeze_revisit_gun_idles_1.wem
|
||||
Speech_MUSEUM_C2_CH4_Freeze_revisit_gun_idles_2.wem
|
||||
Speech_SIDE_CAT_MEET_Freeze_1.wem
|
||||
Speech_SIDE_CAT_MEET_Freeze_2.wem
|
||||
Speech_SIDE_STORY_Nora_Freeze_1.wem
|
||||
Speech_SIDE_STORY_Nora_Freeze_2.wem
|
||||
Speech_SIDE_STORY_Nora_Freeze_3.wem
|
||||
Speech_SIDE_STORY_Nora_Freeze_BM_1.wem
|
||||
Speech_SIDE_STORY_Nora_Freeze_Bmthanks_1.wem
|
||||
Speech_SIDE_STORY_Nora_Freeze_Bmthanks_2.wem
|
||||
Speech_SIDE_STORY_Nora_Freeze_Bmthanks_3.wem
|
||||
Speech_SIDE_STORY_Nora_Freeze_idles_1.wem
|
||||
Speech_SIDE_STORY_Nora_Freeze_idles_2.wem
|
||||
Speech_SIDE_STORY_NORA_Freeze_post_fight_1.wem
|
||||
Speech_SIDE_STORY_Nora_GCPD_NoNora_1.wem
|
||||
Speech_SIDE_STORY_Nora_GCPD_NoNora_2.wem
|
||||
Speech_SIDE_STORY_Nora_GCPD_Nora_2.wem
|
||||
Speech_SIDE_STORY_Nora_GCPD_Nora_4.wem
|
||||
2
data/catalog/source/ita/Gordon.txt
Normal file
2
data/catalog/source/ita/Gordon.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
CHR_RadioScanner_24.wem
|
||||
CHR_RadioScanner_87.wem
|
||||
54
data/catalog/source/ita/Ivy.txt
Normal file
54
data/catalog/source/ita/Ivy.txt
Normal file
@@ -0,0 +1,54 @@
|
||||
Speech_CHAR_CATWOMAN_CW2_Round2_over_1.wem
|
||||
Speech_CHAR_IVY_CW2_PI_Gas_idles_1.wem
|
||||
Speech_CHAR_IVY_CW2_PI_Gas_idles_2.wem
|
||||
Speech_CHAR_IVY_CW2_PI_Gas_idles_3.wem
|
||||
Speech_CHAR_IVY_CW2_PI_Gas_idles_4.wem
|
||||
Speech_CHAR_IVY_Order_guards_1.wem
|
||||
Speech_CHAR_IVY_Order_guards_2.wem
|
||||
Speech_CHAR_IVY_Order_guards_3.wem
|
||||
Speech_CHAR_IVY_Order_guards_4.wem
|
||||
Speech_CHAR_IVY_Order_guards_5.wem
|
||||
Speech_CHAR_IVY_Order_guards_6.wem
|
||||
Speech_CHAR_IVY_Order_guards_7.wem
|
||||
Speech_CHAR_IVY_Order_guards_8.wem
|
||||
Speech_CHAR_IVY_Order_guards_9.wem
|
||||
Speech_CHAR_IVY_Order_guards_10.wem
|
||||
Speech_CHAR_IVY_Order_guards_11.wem
|
||||
Speech_CHAR_IVY_Order_guards_12.wem
|
||||
Speech_CHAR_IVY_Order_guards_13.wem
|
||||
Speech_CHAR_IVY_Plant_grabs_1.wem
|
||||
Speech_CHAR_IVY_Plant_grabs_2.wem
|
||||
Speech_CHAR_IVY_Plant_grabs_3.wem
|
||||
Speech_CHAR_IVY_Plant_grabs_4.wem
|
||||
Speech_CHAR_IVY_Plant_grabs_5.wem
|
||||
Speech_CHAR_IVY_Plant_grabs_6.wem
|
||||
Speech_CHAR_IVY_Plant_grabs_7.wem
|
||||
Speech_GAME_OVER_Poison_Ivy_1.wem
|
||||
Speech_GAME_OVER_Poison_Ivy_2.wem
|
||||
Speech_GAME_OVER_Poison_Ivy_3.wem
|
||||
Speech_GAME_OVER_Poison_Ivy_4.wem
|
||||
Speech_SEQUENCE_CAT_GOTO_IVY_4.wem
|
||||
Speech_SEQUENCE_CAT_GOTO_IVY_8.wem
|
||||
Speech_SEQUENCE_CAT_IVY_1_2.wem
|
||||
Speech_SEQUENCE_CAT_IVY_1_4.wem
|
||||
Speech_SEQUENCE_CAT_IVY_2_2.wem
|
||||
Speech_SEQUENCE_CAT_IVY_2_4.wem
|
||||
Speech_SEQUENCE_CAT_IVY_2_5.wem
|
||||
Speech_SEQUENCE_CAT_IVY_2_6.wem
|
||||
Speech_SIDE_CAT_CH6_cw_ivy_chat_1.wem
|
||||
Speech_SIDE_CAT_CH6_cw_ivy_chat_2.wem
|
||||
Speech_SIDE_CAT_CH6_ivy_idles_1.wem
|
||||
Speech_SIDE_CAT_CH6_ivy_idles_2.wem
|
||||
Speech_SIDE_CAT_CH8_Ivy_Idles_1.wem
|
||||
Speech_SIDE_CAT_CH8_Ivy_Idles_2.wem
|
||||
Speech_SIDE_CAT_CH8_Ivy_Idles_3.wem
|
||||
Speech_SIDE_CAT_CH8b_ivy_chat_4.wem
|
||||
Speech_SIDE_CAT_CH8b_ivy_chat_5.wem
|
||||
Speech_SIDE_CAT_CH8b_ivy_chat_6.wem
|
||||
Speech_SIDE_CAT_CH8b_ivy_chat_7.wem
|
||||
Speech_SIDE_CAT_Post_CW_IVY_IDLES_3.wem
|
||||
Speech_SIDE_CAT_Post_CW_IVY_IDLES_4.wem
|
||||
Speech_SIDE_CAT_Post_CW_IVY_IDLES_5.wem
|
||||
Speech_SIDE_CAT_Post_CW_IVY_IDLES_9.wem
|
||||
Speech_SIDE_CAT_Post_CW_IVY_IDLES_11.wem
|
||||
Speech_SIDE_CAT_Post_CW_IVY_IDLES_12.wem
|
||||
289
data/catalog/source/ita/Joker.txt
Normal file
289
data/catalog/source/ita/Joker.txt
Normal file
@@ -0,0 +1,289 @@
|
||||
Speech_Boss_Clayface_JOKER_1.wem
|
||||
Speech_Boss_Clayface_JOKER_2.wem
|
||||
Speech_Boss_Clayface_JOKER_3.wem
|
||||
Speech_Boss_Clayface_JOKER_4.wem
|
||||
Speech_Boss_Clayface_JOKER_5.wem
|
||||
Speech_Boss_Clayface_JOKER_6.wem
|
||||
Speech_Boss_Clayface_JOKER_BM_gets_sword_1.wem
|
||||
Speech_Boss_Clayface_JOKER_BM_gets_sword_2.wem
|
||||
Speech_Boss_Clayface_JOKER_Cutting_CF_1.wem
|
||||
Speech_Boss_Clayface_JOKER_Detonate_1.wem
|
||||
Speech_Boss_Clayface_JOKER_Detonate_2.wem
|
||||
Speech_Boss_Clayface_JOKER_Detonate_3.wem
|
||||
Speech_Boss_Clayface_JOKER_Detonate_full_1.wem
|
||||
Speech_CHAR_JOKER_APPROACH_STEEL_PA_1.wem
|
||||
Speech_CHAR_JOKER_APPROACH_STEEL_PA_2.wem
|
||||
Speech_CHAR_JOKER_APPROACH_STEEL_PA_3.wem
|
||||
Speech_CHAR_JOKER_APPROACH_STEEL_PA_4.wem
|
||||
Speech_CHAR_JOKER_CH5_working_with_freeze_1.wem
|
||||
Speech_CHAR_JOKER_CH5_working_with_freeze_2.wem
|
||||
Speech_CHAR_JOKER_CH5_working_with_freeze_3.wem
|
||||
Speech_CHAR_JOKER_CH6_SteelMill_noentry_1.wem
|
||||
Speech_CHAR_JOKER_CH6_SteelMill_noentry_2.wem
|
||||
Speech_CHAR_JOKER_CH6_SteelMill_noentry_3.wem
|
||||
Speech_CHAR_JOKER_Come_to_theatre_1.wem
|
||||
Speech_CHAR_JOKER_Come_to_theatre_2.wem
|
||||
Speech_CHAR_JOKER_Come_to_theatre_3.wem
|
||||
Speech_CHAR_JOKER_Come_to_theatre_4.wem
|
||||
Speech_CHAR_JOKER_Come_to_theatre_5.wem
|
||||
Speech_CHAR_JOKER_Come_to_theatre_6.wem
|
||||
Speech_CHAR_JOKER_Come_to_theatre_7.wem
|
||||
Speech_CHAR_JOKER_Leaving_to_Movie_1.wem
|
||||
Speech_CHAR_JOKER_Prank_Killing_1.wem
|
||||
Speech_CHAR_JOKER_Prank_Killing_2.wem
|
||||
Speech_CHAR_JOKER_Prank_Killing_3.wem
|
||||
Speech_CHURCH_B1_CH2_Joker_countdown_1.wem
|
||||
Speech_CHURCH_B1_CH2_Joker_countdown_2.wem
|
||||
Speech_CHURCH_B1_CH2_Joker_countdown_3.wem
|
||||
Speech_CHURCH_B1_CH2_Joker_countdown_4.wem
|
||||
Speech_CHURCH_B1_CH2_Joker_countdown_5.wem
|
||||
Speech_CHURCH_B1_CH2_Joker_countdown_6.wem
|
||||
Speech_CHURCH_B1_Post_zero_joker_message_1.wem
|
||||
Speech_CHURCH_B1_Post_zero_joker_message_2.wem
|
||||
Speech_CHURCH_B1_Post_zero_joker_message_3.wem
|
||||
Speech_CINEMATIC_CH1_CATWOMAN_SAVED_6.wem
|
||||
Speech_CINEMATIC_CH1_CATWOMAN_SAVED_16.wem
|
||||
Speech_CINEMATIC_CH6_BATMAN_CONFRONTS_JOKER_1.wem
|
||||
Speech_CINEMATIC_CH6_BATMAN_CONFRONTS_JOKER_2.wem
|
||||
Speech_CINEMATIC_CH8_TALIA_KILLS_THE_WRONG_JOKER_2.wem
|
||||
Speech_CINEMATIC_CH8_TALIA_KILLS_THE_WRONG_JOKER_3.wem
|
||||
Speech_CINEMATIC_CH8_TALIA_KILLS_THE_WRONG_JOKER_6.wem
|
||||
Speech_CINEMATIC_CH8_TALIA_KILLS_THE_WRONG_JOKER_8.wem
|
||||
Speech_CINEMATIC_CH8_TALIA_KILLS_THE_WRONG_JOKER_9.wem
|
||||
Speech_CINEMATIC_CH8_TALIA_KILLS_THE_WRONG_JOKER_11.wem
|
||||
Speech_CINEMATIC_CH8_TALIA_KILLS_THE_WRONG_JOKER_12.wem
|
||||
Speech_CINEMATIC_CH8_TALIA_KILLS_THE_WRONG_JOKER_16.wem
|
||||
Speech_CINEMATIC_CH8_TALIA_KILLS_THE_WRONG_JOKER_17.wem
|
||||
Speech_CINEMATIC_CH8_TALIA_KILLS_THE_WRONG_JOKER_19.wem
|
||||
Speech_CINEMATIC_CH8_TALIA_KILLS_THE_WRONG_JOKER_20.wem
|
||||
Speech_CINEMATIC_CH8_TALIA_KILLS_THE_WRONG_JOKER_22.wem
|
||||
Speech_CINEMATIC_CH8_TALIA_KILLS_THE_WRONG_JOKER_23.wem
|
||||
Speech_CINEMATIC_CH8_TALIA_KILLS_THE_WRONG_JOKER_24.wem
|
||||
Speech_CINEMATIC_CH8_TALIA_KILLS_THE_WRONG_JOKER_25.wem
|
||||
Speech_CINEMATIC_CH8_TALIA_KILLS_THE_WRONG_JOKER_29.wem
|
||||
Speech_CINEMATIC_CH8_TALIA_KILLS_THE_WRONG_JOKER_36.wem
|
||||
Speech_DLC_DLCComb_1_1.wem
|
||||
Speech_DLC_DLCComb_1_2.wem
|
||||
Speech_DLC_DLCComb_1_3.wem
|
||||
Speech_DLC_DLCComb_1_4.wem
|
||||
Speech_DLC_DLCComb_1_5.wem
|
||||
Speech_DLC_DLCComb_1_6.wem
|
||||
Speech_DLC_DLCComb_1_7.wem
|
||||
Speech_DLC_DLCComb_1_8.wem
|
||||
Speech_DLC_DLCComb_1_9.wem
|
||||
Speech_DLC_DLCComb_1_10.wem
|
||||
Speech_DLC_DLCComb_1_11.wem
|
||||
Speech_DLC_DLCComb_1_12.wem
|
||||
Speech_DLC_DLCComb_1_13.wem
|
||||
Speech_DLC_DLCComb_1_14.wem
|
||||
Speech_DLC_DLCComb_1_15.wem
|
||||
Speech_DLC_DLCComb_1_16.wem
|
||||
Speech_DLC_DLCComb_1_17.wem
|
||||
Speech_DLC_DLCComb_1_18.wem
|
||||
Speech_DLC_DLCComb_1_19.wem
|
||||
Speech_DLC_DLCComb_1_20.wem
|
||||
Speech_DLC_DLCComb_1_21.wem
|
||||
Speech_DLC_DLCComb_1_22.wem
|
||||
Speech_DLC_DLCComb_1_23.wem
|
||||
Speech_DLC_DLCComb_1_24.wem
|
||||
Speech_DLC_DLCComb_1_25.wem
|
||||
Speech_DLC_DLCComb_1_26.wem
|
||||
Speech_DLC_DLCComb_1_27.wem
|
||||
Speech_DLC_DLCComb_1_28.wem
|
||||
Speech_DLC_DLCComb_1_29.wem
|
||||
Speech_DLC_DLCComb_1_30.wem
|
||||
Speech_DLC_DLCComb_1_31.wem
|
||||
Speech_DLC_DLCComb_1_32.wem
|
||||
Speech_DLC_DLCComb_1_33.wem
|
||||
Speech_DLC_DLCComb_1_34.wem
|
||||
Speech_DLC_DLCComb_1_35.wem
|
||||
Speech_DLC_DLCComb_1_36.wem
|
||||
Speech_DLC_DLCComb_1_37.wem
|
||||
Speech_DLC_DLCComb_1_38.wem
|
||||
Speech_DLC_DLCComb_1_39.wem
|
||||
Speech_DLC_DLCComb_1_40.wem
|
||||
Speech_DLC_DLCComb_1_41.wem
|
||||
Speech_DLC_DLCComb_1_42.wem
|
||||
Speech_DLC_DLCComb_1_43.wem
|
||||
Speech_DLC_DLCComb_1_44.wem
|
||||
Speech_DLC_DLCComb_1_45.wem
|
||||
Speech_DLC_DLCComb_1_46.wem
|
||||
Speech_DLC_DLCComb_1_47.wem
|
||||
Speech_DLC_DLCComb_1_48.wem
|
||||
Speech_DLC_DLCComb_1_49.wem
|
||||
Speech_DLC_DLCComb_1_50.wem
|
||||
Speech_DLC_DLCComb_1_51.wem
|
||||
Speech_DLC_DLCComb_1_52.wem
|
||||
Speech_DLC_DLCComb_1_53.wem
|
||||
Speech_DLC_DLCComb_1_54.wem
|
||||
Speech_DLC_DLCComb_1_55.wem
|
||||
Speech_DLC_DLCComb_1_56.wem
|
||||
Speech_DLC_DLCComb_1_57.wem
|
||||
Speech_DLC_DLCComb_1_58.wem
|
||||
Speech_DLC_DLCComb_1_59.wem
|
||||
Speech_DLC_DLCComb_1_60.wem
|
||||
Speech_DLC_DLCComb_1_61.wem
|
||||
Speech_DLC_DLCComb_1_62.wem
|
||||
Speech_DLC_DLCComb_1_63.wem
|
||||
Speech_DLC_DLCComb_1_64.wem
|
||||
Speech_DLC_DLCComb_1_65.wem
|
||||
Speech_DLC_DLCComb_1_66.wem
|
||||
Speech_DLC_DLCComb_1_67.wem
|
||||
Speech_DLC_DLCComb_1_68.wem
|
||||
Speech_DLC_DLCComb_1_69.wem
|
||||
Speech_DLC_DLCComb_1_70.wem
|
||||
Speech_DLC_DLCComb_1_71.wem
|
||||
Speech_DLC_DLCComb_1_72.wem
|
||||
Speech_DLC_DLCComb_1_73.wem
|
||||
Speech_DLC_DLCComb_1_74.wem
|
||||
Speech_DLC_DLCComb_1_75.wem
|
||||
Speech_DLC_DLCComb_1_76.wem
|
||||
Speech_DLC_DLCComb_1_77.wem
|
||||
Speech_DLC_DLCComb_1_78.wem
|
||||
Speech_DLC_DLCComb_1_79.wem
|
||||
Speech_DLC_DLCComb_1_80.wem
|
||||
Speech_DLC_DLCComb_1_81.wem
|
||||
Speech_DLC_DLCComb_1_82.wem
|
||||
Speech_DLC_DLCComb_1_83.wem
|
||||
Speech_DLC_DLCComb_1_84.wem
|
||||
Speech_DLC_DLCComb_1_85.wem
|
||||
Speech_DLC_DLCComb_1_86.wem
|
||||
Speech_DLC_DLCComb_1_87.wem
|
||||
Speech_DLC_DLCComb_1_88.wem
|
||||
Speech_DLC_DLCComb_1_89.wem
|
||||
Speech_DLC_DLCComb_1_90.wem
|
||||
Speech_DLC_DLCComb_1_91.wem
|
||||
Speech_DLC_DLCComb_1_92.wem
|
||||
Speech_DLC_DLCComb_1_93.wem
|
||||
Speech_DLC_DLCComb_1_94.wem
|
||||
Speech_DLC_DLCComb_1_95.wem
|
||||
Speech_DLC_DLCComb_1_96.wem
|
||||
Speech_DLC_DLCComb_1_97.wem
|
||||
Speech_DLC_DLCComb_1_98.wem
|
||||
Speech_DLC_DLCComb_1_99.wem
|
||||
Speech_DLC_DLCComb_1_100.wem
|
||||
Speech_DLC_DLCComb_1_101.wem
|
||||
Speech_DLC_DLCComb_1_102.wem
|
||||
Speech_DLC_DLCComb_1_103.wem
|
||||
Speech_DLC_DLCComb_1_104.wem
|
||||
Speech_DLC_DLCComb_1_105.wem
|
||||
Speech_DLC_DLCComb_1_106.wem
|
||||
Speech_DLC_DLCComb_1_107.wem
|
||||
Speech_DLC_DLCComb_1_INTRO_cast_1.wem
|
||||
Speech_DLC_DLCComb_1_INTRO_cast_2.wem
|
||||
Speech_DLC_DLCComb_1_INTRO_cast_3.wem
|
||||
Speech_DLC_DLCComb_1_INTRO_cast_4.wem
|
||||
Speech_DLC_DLCComb_1_INTRO_cast_5.wem
|
||||
Speech_DLC_DLCComb_1_INTRO_cast_6.wem
|
||||
Speech_DLC_DLCComb_1_INTRO_cast_7.wem
|
||||
Speech_DLC_DLCComb_1_INTRO_cast_8.wem
|
||||
Speech_DLC_DLCComb_1_INTRO_cast_9.wem
|
||||
Speech_DLC_DLCComb_1_INTRO_cast_10.wem
|
||||
Speech_DLC_DLCComb_1_INTRO_cast_11.wem
|
||||
Speech_DLC_DLCComb_1_INTRO_cast_12.wem
|
||||
Speech_DLC_DLCComb_1_INTRO_cast_13.wem
|
||||
Speech_DLC_DLCComb_1_INTRO_cast_14.wem
|
||||
Speech_GAME_OVER_Joker_1.wem
|
||||
Speech_GAME_OVER_Joker_2.wem
|
||||
Speech_GAME_OVER_Joker_3.wem
|
||||
Speech_GAME_OVER_Joker_4.wem
|
||||
Speech_GAME_OVER_Joker_5.wem
|
||||
Speech_GAME_OVER_Joker_6.wem
|
||||
Speech_GAME_OVER_Joker_7.wem
|
||||
Speech_GAME_OVER_Joker_8.wem
|
||||
Speech_GAME_OVER_Joker_9.wem
|
||||
Speech_GAME_OVER_Joker_10.wem
|
||||
Speech_GAME_OVER_Joker_11.wem
|
||||
Speech_GAME_OVER_Joker_12.wem
|
||||
Speech_GAME_OVER_Joker_13.wem
|
||||
Speech_GAME_OVER_Joker_14.wem
|
||||
Speech_GAME_OVER_Joker_15.wem
|
||||
Speech_GAME_OVER_Joker_16.wem
|
||||
Speech_GAME_OVER_Joker_17.wem
|
||||
Speech_GCPD_Joker_laugh_1.wem
|
||||
Speech_GCPD_Joker_laugh_2.wem
|
||||
Speech_PRED_J_Automated_1.wem
|
||||
Speech_PRED_J_Automated_2.wem
|
||||
Speech_PRED_J_Automated_3.wem
|
||||
Speech_PRED_J_Automated_4.wem
|
||||
Speech_PRED_J_Automated_5.wem
|
||||
Speech_PRED_J_Automated_6.wem
|
||||
Speech_PRED_J_Automated_7.wem
|
||||
Speech_PRED_J_Automated_8.wem
|
||||
Speech_PRED_J_Automated_9.wem
|
||||
Speech_PRED_J_Automated_10.wem
|
||||
Speech_PRED_J_Automated_11.wem
|
||||
Speech_PRED_J_Automated_12.wem
|
||||
Speech_PRED_J_Automated_13.wem
|
||||
Speech_PRED_J_Automated_14.wem
|
||||
Speech_PRED_J_Automated_15.wem
|
||||
Speech_PRED_J_Automated_16.wem
|
||||
Speech_PRED_J_Automated_17.wem
|
||||
Speech_PRED_J_Automated_18.wem
|
||||
Speech_PRED_J_Automated_19.wem
|
||||
Speech_PRED_J_Automated_20.wem
|
||||
Speech_PRED_J_Automated_21.wem
|
||||
Speech_PRED_J_Automated_22.wem
|
||||
Speech_PRED_J_Automated_23.wem
|
||||
Speech_PRED_J_Automated_24.wem
|
||||
Speech_PRED_J_Automated_25.wem
|
||||
Speech_PRED_J_Automated_26.wem
|
||||
Speech_PRED_J_Automated_27.wem
|
||||
Speech_PRED_J_Automated_28.wem
|
||||
Speech_PRED_J_Automated_29.wem
|
||||
Speech_PRED_J_Automated_30.wem
|
||||
Speech_PRED_J_Automated_31.wem
|
||||
Speech_PRED_J_Automated_32.wem
|
||||
Speech_PRED_J_Automated_33.wem
|
||||
Speech_PRED_J_Automated_34.wem
|
||||
Speech_PRED_J_Automated_35.wem
|
||||
Speech_PRED_J_Automated_36.wem
|
||||
Speech_PRED_J_Automated_37.wem
|
||||
Speech_PRED_J_Automated_38.wem
|
||||
Speech_PRED_J_Automated_39.wem
|
||||
Speech_PRED_J_Automated_40.wem
|
||||
Speech_PRED_J_Automated_41.wem
|
||||
Speech_PRED_J_Automated_42.wem
|
||||
Speech_PRED_J_Automated_43.wem
|
||||
Speech_PRED_J_Automated_44.wem
|
||||
Speech_PRED_J_Automated_45.wem
|
||||
Speech_PRED_J_Automated_46.wem
|
||||
Speech_PRED_J_Automated_47.wem
|
||||
Speech_PRED_J_Automated_48.wem
|
||||
Speech_PRED_J_Automated_49.wem
|
||||
Speech_PRED_J_Automated_50.wem
|
||||
Speech_PRED_J_CH4_Ending_1.wem
|
||||
Speech_PRED_J_CH4_Ending_2.wem
|
||||
Speech_PRED_J_CH4_Ending_3.wem
|
||||
Speech_PRED_J_CH5_GCPD_END_2.wem
|
||||
Speech_PRED_J_CH5_GCPD_END_3.wem
|
||||
Speech_PRED_J_CH6_ending_1.wem
|
||||
Speech_PRED_J_CH6_ending_2.wem
|
||||
Speech_PRED_J_CH6_ending_3.wem
|
||||
Speech_PRED_J_CH6_Steel_ext_end_1.wem
|
||||
Speech_SEQUENCE_CH3_BM_WAKES_INTRO_1.wem
|
||||
Speech_SEQUENCE_CH3_BM_WAKES_INTRO_6.wem
|
||||
Speech_SEQUENCE_CH3_BM_WAKES_INTRO_8.wem
|
||||
Speech_SEQUENCE_CH3_BM_WAKES_INTRO_9.wem
|
||||
Speech_SIDE_STORY_VV_saved_conv_1.wem
|
||||
Speech_SIDE_STORY_VV_saved_conv_8.wem
|
||||
Speech_SIDE_STORY_VV_saved_conv_9.wem
|
||||
Speech_SIDE_STORY_VV_Shoot_copter_3.wem
|
||||
Speech_SIDE_STORY_VV_Shoot_copter_4.wem
|
||||
Speech_STEEL_B1_CH2_HQ_Intro3_1.wem
|
||||
Speech_STEEL_B1_CH2_HQ_Muffled_Conversation_2.wem
|
||||
Speech_STEEL_B1_CH2_HQ_vis2_managersoffice_1.wem
|
||||
Speech_STEEL_B1_CH2_HQ_vis2_managersoffice_interrupt_2.wem
|
||||
Speech_STEEL_B3_Ch6_fight_intro2_1.wem
|
||||
Speech_STEEL_B3_Ch6_fight_intro2_2.wem
|
||||
Speech_STEEL_B3_Ch6_fight_intro_1.wem
|
||||
Speech_STEEL_B3_Ch6_fight_intro_2.wem
|
||||
Speech_STEEL_B3_CH6_Joker_Lieut_1.wem
|
||||
Speech_STEEL_B3_CH6_Joker_Lieut_2.wem
|
||||
Speech_STEEL_B3_CH6_Joker_Titan_1.wem
|
||||
Speech_STREETS_A5_CH8_BM_OR_Catchup_5.wem
|
||||
Speech_TV_CH2_JK_belfry_1.wem
|
||||
Speech_TV_CH2_JK_belfry_2.wem
|
||||
Speech_TV_CH2_JK_belfry_3.wem
|
||||
Speech_TV_CH2_JK_belfry_4.wem
|
||||
Speech_TV_CH2_JK_belfry_5.wem
|
||||
40
data/catalog/source/ita/Madhatter.txt
Normal file
40
data/catalog/source/ita/Madhatter.txt
Normal file
@@ -0,0 +1,40 @@
|
||||
Speech_GAME_OVER_MadHatter_1.wem
|
||||
Speech_GAME_OVER_MadHatter_2.wem
|
||||
Speech_SIDE_CAT_MEET_Hatter_1.wem
|
||||
Speech_SIDE_CAT_MEET_Hatter_4.wem
|
||||
Speech_SIDE_MADHATTER_Break_control_1.wem
|
||||
Speech_SIDE_MADHATTER_Break_control_2.wem
|
||||
Speech_SIDE_MADHATTER_BREAKHAT_1.wem
|
||||
Speech_SIDE_MADHATTER_FIGHT_IDLES_1.wem
|
||||
Speech_SIDE_MADHATTER_FIGHT_IDLES_2.wem
|
||||
Speech_SIDE_MADHATTER_FIGHT_IDLES_3.wem
|
||||
Speech_SIDE_MADHATTER_FIGHT_IDLES_4.wem
|
||||
Speech_SIDE_MADHATTER_FIGHT_IDLES_5.wem
|
||||
Speech_SIDE_MADHATTER_FIGHT_IDLES_6.wem
|
||||
Speech_SIDE_MADHATTER_FIGHT_IDLES_7.wem
|
||||
Speech_SIDE_MADHATTER_HYNO_IDLES_1.wem
|
||||
Speech_SIDE_MADHATTER_HYNO_IDLES_2.wem
|
||||
Speech_SIDE_MADHATTER_HYNO_IDLES_3.wem
|
||||
Speech_SIDE_MADHATTER_HYNO_IDLES_4.wem
|
||||
Speech_SIDE_MADHATTER_HYNO_IDLES_5.wem
|
||||
Speech_SIDE_MADHATTER_HYNO_IDLES_6.wem
|
||||
Speech_SIDE_MADHATTER_HYNO_IDLES_7.wem
|
||||
Speech_SIDE_MADHATTER_HYNO_IDLES_8.wem
|
||||
Speech_SIDE_MADHATTER_HYNO_IDLES_9.wem
|
||||
Speech_SIDE_MADHATTER_HYNO_IDLES_10.wem
|
||||
Speech_SIDE_MADHATTER_HYNO_IDLES_11.wem
|
||||
Speech_SIDE_MADHATTER_INTRO1_1.wem
|
||||
Speech_SIDE_MADHATTER_INTRO1_3.wem
|
||||
Speech_SIDE_MADHATTER_INTRO1_5.wem
|
||||
Speech_SIDE_MADHATTER_INTRO1_6.wem
|
||||
Speech_SIDE_MADHATTER_INTRO2_1.wem
|
||||
Speech_SIDE_MADHATTER_INTRO3_1.wem
|
||||
Speech_SIDE_MADHATTER_INTRO4_1.wem
|
||||
Speech_SIDE_MADHATTER_MY_SOLDIER_1.wem
|
||||
Speech_SIDE_MADHATTER_MY_SOLDIER_2.wem
|
||||
Speech_SIDE_MADHATTER_MY_SOLDIER_3.wem
|
||||
Speech_SIDE_MADHATTER_POST_FIGHT_1.wem
|
||||
Speech_SIDE_MADHATTER_POST_FIGHT_IDLES_1.wem
|
||||
Speech_SIDE_MADHATTER_POST_FIGHT_IDLES_2.wem
|
||||
Speech_SIDE_MADHATTER_POST_FIGHT_IDLES_3.wem
|
||||
Speech_SIDE_MADHATTER_SOBBING_1.wem
|
||||
83
data/catalog/source/ita/Oracle.txt
Normal file
83
data/catalog/source/ita/Oracle.txt
Normal file
@@ -0,0 +1,83 @@
|
||||
Speech_CHAR_BATMAN_No_entry_chimney_1.wem
|
||||
Speech_CHAR_BATMAN_No_entry_chimney_5.wem
|
||||
Speech_CHAR_BATMAN_No_entry_chimney_6.wem
|
||||
Speech_GCPD_Boss_intro_1.wem
|
||||
Speech_MAP_BM_OR_recogniseshot2_map_1.wem
|
||||
Speech_MAP_BM_OR_recogniseshot2_map_2.wem
|
||||
Speech_MAP_BM_OR_recogniseshot2_map_3.wem
|
||||
Speech_MAP_BM_OR_recogniseshot2_map_4.wem
|
||||
Speech_MAP_BM_OR_recogniseshot_map_1.wem
|
||||
Speech_MAP_BM_OR_recogniseshot_map_2.wem
|
||||
Speech_MAP_BM_OR_recogniseshot_map_3.wem
|
||||
Speech_MUSEUM_B1_CH3_BM_ALF_ORA_Catchup_4.wem
|
||||
Speech_MUSEUM_B1_CH3_BM_ALF_ORA_Catchup_6.wem
|
||||
Speech_MUSEUM_C1_CH4_BM_ORACLE_NINJAS_1.wem
|
||||
Speech_MUSEUM_C1_CH4_BM_ORACLE_NINJAS_2.wem
|
||||
Speech_MUSEUM_C1_CH4_BM_ORACLE_NINJAS_3.wem
|
||||
Speech_RADIO_SCAN_Secret1_1.wem
|
||||
Speech_RADIO_SCAN_Secret1_2.wem
|
||||
Speech_RADIO_SCAN_Secret1_3.wem
|
||||
Speech_RADIO_SCAN_Secret1_4.wem
|
||||
Speech_RADIO_SCAN_Secret1_5.wem
|
||||
Speech_RADIO_SCAN_Secret1FULL_1.wem
|
||||
Speech_RIDDLER_R5_new_finished_bm_contact_Oracle_1.wem
|
||||
Speech_RIDDLER_R5_new_finished_bm_contact_Oracle_5.wem
|
||||
Speech_RIDDLER_R5_new_finished_oracle_give_location_1.wem
|
||||
Speech_RIDDLER_R5_new_finished_oracle_give_location_4.wem
|
||||
Speech_SIDE_DEADSHOT_BM_OR_recogniseshot2_1.wem
|
||||
Speech_SIDE_DEADSHOT_BM_OR_recogniseshot_2.wem
|
||||
Speech_SIDE_DEADSHOT_BM_OR_recogniseshot_3.wem
|
||||
Speech_SIDE_DEADSHOT_END_seq_2.wem
|
||||
Speech_SIDE_DEADSHOT_VIC2_BM_OR_CONV1_2.wem
|
||||
Speech_SIDE_DEADSHOT_VIC2_BM_OR_CONV1_5.wem
|
||||
Speech_SIDE_DEADSHOT_VIC2_BM_OR_CONV2_3.wem
|
||||
Speech_SIDE_DEADSHOT_VIC2_BM_OR_CONV2_4.wem
|
||||
Speech_SIDE_DEADSHOT_VIC3_BM_OR_CONV1_2.wem
|
||||
Speech_SIDE_DEADSHOT_VIC3_BM_OR_CONV1_5.wem
|
||||
Speech_SIDE_DEADSHOT_VIC3_BM_OR_CONV1_6.wem
|
||||
Speech_SIDE_DEADSHOT_VIC3_BM_OR_CONV2_3.wem
|
||||
Speech_SIDE_DEADSHOT_VIC3_BM_OR_CONV2_4.wem
|
||||
Speech_SIDE_HUSH_BM_post_hush_OR_1.wem
|
||||
Speech_SIDE_HUSH_BM_post_hush_OR_2.wem
|
||||
Speech_SIDE_HUSH_BM_post_hush_OR_6.wem
|
||||
Speech_SIDE_HUSH_VIC2_BOTH_SCANNED_1.wem
|
||||
Speech_SIDE_HUSH_VIC2_BOTH_SCANNED_2.wem
|
||||
Speech_SIDE_HUSH_VIC2_BOTH_SCANNED_POST_3.wem
|
||||
Speech_SIDE_HUSH_VIC2_BOTH_SCANNED_POST_4.wem
|
||||
Speech_SIDE_HUSH_VIC2_BOTH_SCANNED_POST_5.wem
|
||||
Speech_SIDE_HUSH_VIC2_BOTH_SCANNED_POST_7.wem
|
||||
Speech_SIDE_HUSH_VIC2_SCANPRINT_2.wem
|
||||
Speech_STEEL_C4_OR_INT_COUNCIL_1_1.wem
|
||||
Speech_STEEL_C4_OR_INT_COUNCIL_1_3.wem
|
||||
Speech_STEEL_C4_OR_INT_COUNCIL_1_4.wem
|
||||
Speech_STEEL_C5_OR_INT_COUNCIL_2_1.wem
|
||||
Speech_STEEL_C5_OR_INT_COUNCIL_2_3.wem
|
||||
Speech_STEEL_C5_OR_INT_COUNCIL_2_4.wem
|
||||
Speech_STREETS_A3_CH7_BM_ALF_Tower_1.wem
|
||||
Speech_STREETS_A3_CH7_NEW_DILEMMA_4.wem
|
||||
Speech_STREETS_A4_CH4_BM_Trail_underground_1.wem
|
||||
Speech_STREETS_A5_CH5_BM_OR_Mayor_1.wem
|
||||
Speech_STREETS_A5_CH5_BM_OR_Mayor_3.wem
|
||||
Speech_STREETS_A5_CH5_post_mayor_BM_ALF_1.wem
|
||||
Speech_STREETS_A5_CH5_post_mayor_BM_ALF_3.wem
|
||||
Speech_STREETS_A5_CH5_post_mayor_BM_ALF_5.wem
|
||||
Speech_STREETS_A5_CH8_BM_OR_Catchup_4.wem
|
||||
Speech_STREETS_A5_CH8_BM_OR_Catchup_6.wem
|
||||
Speech_STREETS_A5_CH8_BM_OR_Catchup_8.wem
|
||||
Speech_UNDER_A1_CH4_Post_BM_OR_1.wem
|
||||
Speech_UNDER_A1_CH4_Post_BM_OR_2.wem
|
||||
Speech_UNDER_A1_CH5_BM_NW_1.wem
|
||||
Speech_UNDER_A1_CH5_BM_NW_2.wem
|
||||
Speech_UNDER_A1_CH5_BM_NW_6.wem
|
||||
Speech_UNDER_A1_CH5_BM_OR_LAZARUS_3.wem
|
||||
Speech_UNDER_A1_CH5_BM_OR_LAZARUS_4.wem
|
||||
Speech_UNDER_A1_CH5_BM_OR_LAZARUS_8.wem
|
||||
Speech_UNDER_A1_CH5_BM_OR_LAZARUS_9.wem
|
||||
Speech_UNDER_A1_CH5_BM_OR_LAZARUS_10.wem
|
||||
Speech_UNDER_B4_CH7_ORACLE_near_top_1.wem
|
||||
Speech_UNDER_B4_CH7_ORACLE_near_top_2.wem
|
||||
Speech_UNDER_C1_CH4_BM_OR_Collapse1_2.wem
|
||||
Speech_UNDER_C1_CH4_BM_OR_Collapse1_3.wem
|
||||
Speech_UNDER_C1_CH4_BM_OR_stealguns_2.wem
|
||||
Speech_UNDER_S1_Sick_Dream_4.wem
|
||||
Speech_UNDER_S1_Sick_dream_post_2.wem
|
||||
296
data/catalog/source/ita/Penguin.txt
Normal file
296
data/catalog/source/ita/Penguin.txt
Normal file
@@ -0,0 +1,296 @@
|
||||
CHR_RadioScanner_18.wem
|
||||
CHR_RadioScanner_55.wem
|
||||
CHR_RadioScanner_90.wem
|
||||
Speech_BOSS_GRUNDY_PENGUIN_1.wem
|
||||
Speech_BOSS_GRUNDY_PENGUIN_2.wem
|
||||
Speech_BOSS_GRUNDY_PENGUIN_3.wem
|
||||
Speech_BOSS_GRUNDY_PENGUIN_4.wem
|
||||
Speech_BOSS_GRUNDY_PENGUIN_5.wem
|
||||
Speech_BOSS_GRUNDY_PENGUIN_6.wem
|
||||
Speech_BOSS_GRUNDY_PENGUIN_7.wem
|
||||
Speech_BOSS_GRUNDY_PENGUIN_8.wem
|
||||
Speech_BOSS_GRUNDY_PENGUIN_9.wem
|
||||
Speech_BOSS_GRUNDY_PENGUIN_10.wem
|
||||
Speech_BOSS_GRUNDY_PENGUIN_11.wem
|
||||
Speech_BOSS_GRUNDY_PENGUIN_12.wem
|
||||
Speech_BOSS_GRUNDY_PENGUIN_13.wem
|
||||
Speech_BOSS_GRUNDY_PENGUIN_14.wem
|
||||
Speech_BOSS_GRUNDY_PENGUIN_15.wem
|
||||
Speech_BOSS_GRUNDY_PENGUIN_16.wem
|
||||
Speech_BOSS_GRUNDY_PENGUIN_17.wem
|
||||
Speech_BOSS_GRUNDY_PENGUIN_18.wem
|
||||
Speech_BOSS_GRUNDY_PENGUIN_Flaming_Grundy_1.wem
|
||||
Speech_BOSS_GRUNDY_PENGUIN_Grundy_is_dead_1.wem
|
||||
Speech_BOSS_GRUNDY_PENGUIN_Grundy_is_dead_2.wem
|
||||
Speech_BOSS_GRUNDY_PENGUIN_Grundy_knees_1_1.wem
|
||||
Speech_BOSS_GRUNDY_PENGUIN_Oi_1.wem
|
||||
Speech_BOSS_GRUNDY_PENGUIN_Over_charge_1.wem
|
||||
Speech_BOSS_GRUNDY_PENGUIN_Penguin_rocket_launcher_1.wem
|
||||
Speech_BOSS_GRUNDY_PENGUIN_Rec_1.wem
|
||||
Speech_CH_ZERO_CH0_fight_intro2_1.wem
|
||||
Speech_CH_ZERO_CH0_fight_intro_1.wem
|
||||
Speech_CH_ZERO_CH0_fight_intro_2.wem
|
||||
Speech_CH_ZERO_CH0_fight_intro_4.wem
|
||||
Speech_CH_ZERO_CH0_Peng_scream_1.wem
|
||||
Speech_CH_ZERO_CH0_Peng_scream_2.wem
|
||||
Speech_CH_ZERO_CH0_Peng_scream_3.wem
|
||||
Speech_CH_ZERO_CH0_Penguin_reveal_1.wem
|
||||
Speech_CH_ZERO_CH0_Penguin_reveal_2.wem
|
||||
Speech_CH_ZERO_CH0_Thug_Knockout_1.wem
|
||||
Speech_CH_ZERO_CH0_Thug_Knockout_5.wem
|
||||
Speech_CH_ZERO_PENG_fightcomments_1.wem
|
||||
Speech_CH_ZERO_PENG_fightcomments_2.wem
|
||||
Speech_CH_ZERO_PENG_fightcomments_3.wem
|
||||
Speech_CH_ZERO_PENG_GATEHELP_1.wem
|
||||
Speech_CH_ZERO_PENG_GATEHELP_2.wem
|
||||
Speech_CH_ZERO_PENG_GATEHELP_3.wem
|
||||
Speech_CH_ZERO_PENG_GATEHELP_4.wem
|
||||
Speech_CH_ZERO_PENG_GATEHELP_5.wem
|
||||
Speech_CH_ZERO_PENG_TRASHTALK2_1.wem
|
||||
Speech_CH_ZERO_PENG_TRASHTALK2_2.wem
|
||||
Speech_CH_ZERO_PENG_TRASHTALK2_3.wem
|
||||
Speech_CINEMATIC_CH3_MEET_THE_PENGUIN_1.wem
|
||||
Speech_CINEMATIC_CH3_MEET_THE_PENGUIN_2.wem
|
||||
Speech_CINEMATIC_CH3_MEET_THE_PENGUIN_3.wem
|
||||
Speech_CINEMATIC_CH3_MEET_THE_PENGUIN_4.wem
|
||||
Speech_CINEMATIC_CH3_MEET_THE_PENGUIN_5.wem
|
||||
Speech_CINEMATIC_CH3_MEET_THE_PENGUIN_6.wem
|
||||
Speech_CINEMATIC_CH3_MEET_THE_PENGUIN_7.wem
|
||||
Speech_CINEMATIC_CH3_MEET_THE_PENGUIN_8.wem
|
||||
Speech_CINEMATIC_CH3_MEET_THE_PENGUIN_9.wem
|
||||
Speech_CINEMATIC_CH3_MEET_THE_PENGUIN_10.wem
|
||||
Speech_CINEMATIC_CH3_MEET_THE_PENGUIN_12.wem
|
||||
Speech_CINEMATIC_CH3_MEET_THE_PENGUIN_15.wem
|
||||
Speech_CINEMATIC_CH3_MEET_THE_PENGUIN_16.wem
|
||||
Speech_CINEMATIC_CH3_MEET_THE_PENGUIN_17.wem
|
||||
Speech_CINEMATIC_CH3_MEET_THE_PENGUIN_18.wem
|
||||
Speech_CINEMATIC_CH3_MEET_THE_PENGUIN_19.wem
|
||||
Speech_DLC_DLCComb_2_1.wem
|
||||
Speech_DLC_DLCComb_2_2.wem
|
||||
Speech_DLC_DLCComb_2_2.wem
|
||||
Speech_DLC_DLCComb_2_3.wem
|
||||
Speech_DLC_DLCComb_2_4.wem
|
||||
Speech_DLC_DLCComb_2_5.wem
|
||||
Speech_DLC_DLCComb_2_6.wem
|
||||
Speech_DLC_DLCComb_2_7.wem
|
||||
Speech_DLC_DLCComb_2_8.wem
|
||||
Speech_DLC_DLCComb_2_9.wem
|
||||
Speech_DLC_DLCComb_2_10.wem
|
||||
Speech_DLC_DLCComb_2_11.wem
|
||||
Speech_DLC_DLCComb_2_12.wem
|
||||
Speech_DLC_DLCComb_2_13.wem
|
||||
Speech_DLC_DLCComb_2_14.wem
|
||||
Speech_DLC_DLCComb_2_INTRO_cast_1.wem
|
||||
Speech_DLC_DLCComb_2_INTRO_cast_2.wem
|
||||
Speech_DLC_DLCComb_2_INTRO_cast_3.wem
|
||||
Speech_DLC_DLCComb_2_INTRO_cast_4.wem
|
||||
Speech_DLC_DLCComb_2_INTRO_cast_5.wem
|
||||
Speech_DLC_DLCComb_2_INTRO_cast_6.wem
|
||||
Speech_DLC_DLCComb_2_INTRO_cast_7.wem
|
||||
Speech_DLC_DLCComb_2_INTRO_cast_8.wem
|
||||
Speech_DLC_DLCComb_2_INTRO_cast_9.wem
|
||||
Speech_DLC_DLCComb_2_INTRO_cast_10.wem
|
||||
Speech_DLC_DLCComb_2_INTRO_cast_11.wem
|
||||
Speech_DLC_DLCComb_2_INTRO_cast_12.wem
|
||||
Speech_DLC_DLCComb_2_INTRO_cast_13.wem
|
||||
Speech_DLC_DLCComb_2_INTRO_cast_14.wem
|
||||
Speech_GAME_OVER_Penguin_1.wem
|
||||
Speech_GAME_OVER_Penguin_2.wem
|
||||
Speech_GAME_OVER_Penguin_3.wem
|
||||
Speech_GAME_OVER_Penguin_4.wem
|
||||
Speech_GAME_OVER_Penguin_5.wem
|
||||
Speech_GCPD_Cackle_1.wem
|
||||
Speech_GCPD_Cackle_2.wem
|
||||
Speech_GCPD_Cackle_3.wem
|
||||
Speech_GCPD_Cackle_4.wem
|
||||
Speech_GCPD_CH3_PENG_post_int_1.wem
|
||||
Speech_GCPD_CH3_Penguin_Radio_2.wem
|
||||
Speech_GCPD_CH3_Penguin_Radio_3.wem
|
||||
Speech_GCPD_CH3_Penguin_Radio_4.wem
|
||||
Speech_GCPD_CH3_Thug_intro_radio_3.wem
|
||||
Speech_GCPD_CH3_thug_P_Lock_1.wem
|
||||
Speech_GCPD_Radio_intro_1.wem
|
||||
Speech_GCPD_Radio_intro_2.wem
|
||||
Speech_GCPD_Radio_intro_3.wem
|
||||
Speech_INTERROGATION_GCPD_A1_01_1.wem
|
||||
Speech_MUSEUM_A1_CH3_Intro_2.wem
|
||||
Speech_MUSEUM_A1_CH3_Intro_4.wem
|
||||
Speech_MUSEUM_B1_CH3_museum_B1_Taunts_R1_1.wem
|
||||
Speech_MUSEUM_B1_CH3_museum_B1_Taunts_R1_2.wem
|
||||
Speech_MUSEUM_B1_CH3_museum_B1_Taunts_R1_3.wem
|
||||
Speech_MUSEUM_B1_CH3_museum_B1_Taunts_R1_4.wem
|
||||
Speech_MUSEUM_B1_CH3_museum_B1_Taunts_R1_5.wem
|
||||
Speech_MUSEUM_B1_CH3_museum_B1_Taunts_R1_6.wem
|
||||
Speech_MUSEUM_B1_CH3_museum_B1_Taunts_R1_7.wem
|
||||
Speech_MUSEUM_B1_CH3_museum_B1_Taunts_R1_8.wem
|
||||
Speech_MUSEUM_B1_CH3_museum_B1_Taunts_R2_1.wem
|
||||
Speech_MUSEUM_B1_CH3_museum_B1_Taunts_R2_2.wem
|
||||
Speech_MUSEUM_B1_CH3_museum_B1_Taunts_R2_3.wem
|
||||
Speech_MUSEUM_B1_CH3_museum_B1_Taunts_R2_4.wem
|
||||
Speech_MUSEUM_B1_CH3_museum_B1_Taunts_R2_5.wem
|
||||
Speech_MUSEUM_B1_CH3_museum_B1_Taunts_R2_6.wem
|
||||
Speech_MUSEUM_B1_CH3_museum_B1_Taunts_R2_7.wem
|
||||
Speech_MUSEUM_B1_CH3_museum_B1_Taunts_R2_8.wem
|
||||
Speech_MUSEUM_B1_CH3_museum_B1_Taunts_R2_9.wem
|
||||
Speech_MUSEUM_B2_CH3_Penguin_Shark_1.wem
|
||||
Speech_MUSEUM_B2_CH3_Penguin_Shark_2.wem
|
||||
Speech_MUSEUM_B2_CH3_Penguin_Shark_3.wem
|
||||
Speech_MUSEUM_B2_CH3_Penguin_tort_1.wem
|
||||
Speech_MUSEUM_B2_CH3_Penguin_tort_2.wem
|
||||
Speech_MUSEUM_B2_CH3_Penguin_tort_3.wem
|
||||
Speech_MUSEUM_B2_CH3_Penguin_tort_4.wem
|
||||
Speech_MUSEUM_B2_CH3_Penguin_tort_5.wem
|
||||
Speech_MUSEUM_B2_CH3_Penguin_tort_6.wem
|
||||
Speech_MUSEUM_C1_CH3_Museum_entry_1.wem
|
||||
Speech_MUSEUM_C1_CH4_Penguin_1.wem
|
||||
Speech_MUSEUM_C1_CH4_Penguin_3.wem
|
||||
Speech_MUSEUM_C1_CH4_Penguin_idle_1.wem
|
||||
Speech_MUSEUM_C1_CH4_Penguin_idle_2.wem
|
||||
Speech_MUSEUM_C1_CH4_Penguin_idle_3.wem
|
||||
Speech_MUSEUM_C1_CH5_to_8_2FTHUG_TO_PENG_5.wem
|
||||
Speech_MUSEUM_C1_CH5_to_8_2FTHUG_TO_PENG_6.wem
|
||||
Speech_MUSEUM_C1_CH5_to_8_2FTHUG_TO_PENG_8.wem
|
||||
Speech_MUSEUM_C1_CH5_to_8_2FTHUG_TO_PENG_9.wem
|
||||
Speech_MUSEUM_C1_CH9_Peng_in_Cab_conv_1.wem
|
||||
Speech_MUSEUM_C1_CH9_Peng_in_Cab_conv_3.wem
|
||||
Speech_MUSEUM_EXHIBIT_A1_BodyArmor_1.wem
|
||||
Speech_MUSEUM_EXHIBIT_A1_Freeze_suit_1.wem
|
||||
Speech_MUSEUM_EXHIBIT_A1_Goggles_1.wem
|
||||
Speech_MUSEUM_EXHIBIT_A1_Grenades_1.wem
|
||||
Speech_MUSEUM_EXHIBIT_A1_Jammers_1.wem
|
||||
Speech_MUSEUM_EXHIBIT_A1_MGuns_1.wem
|
||||
Speech_MUSEUM_EXHIBIT_A1_Mines_1.wem
|
||||
Speech_MUSEUM_EXHIBIT_A1_Rockets_1.wem
|
||||
Speech_MUSEUM_EXHIBIT_A1_SHOTGUN_1.wem
|
||||
Speech_MUSEUM_EXHIBIT_A1_SNIPER_1.wem
|
||||
Speech_MUSEUM_EXHIBIT_C1_ASSASSIN_1.wem
|
||||
Speech_MUSEUM_EXHIBIT_C1_ASSASSIN_2.wem
|
||||
Speech_MUSEUM_EXHIBIT_C1_BATMAN_1.wem
|
||||
Speech_MUSEUM_EXHIBIT_C1_BATMAN_2.wem
|
||||
Speech_MUSEUM_EXHIBIT_C1_Bruce_Wayne_1.wem
|
||||
Speech_MUSEUM_EXHIBIT_C1_Bruce_Wayne_2.wem
|
||||
Speech_MUSEUM_EXHIBIT_C1_Joker_1.wem
|
||||
Speech_MUSEUM_EXHIBIT_C1_scar_1.wem
|
||||
Speech_MUSEUM_EXHIBIT_C1_SCARFACE_1.wem
|
||||
Speech_MUSEUM_EXHIBIT_C1_SCARFACE_2.wem
|
||||
Speech_MUSEUM_EXHIBIT_C1_TWOFACE_1.wem
|
||||
Speech_MUSEUM_EXHIBIT_C1_TYGER_1.wem
|
||||
Speech_MUSEUM_EXHIBIT_C1_Zsasz_1.wem
|
||||
Speech_MUSEUM_EXHIBIT_C1_Zsasz_2.wem
|
||||
Speech_MUSEUM_EXHIBIT_C2_Freeze_1.wem
|
||||
Speech_MUSEUM_EXHIBIT_C2_Freeze_2.wem
|
||||
Speech_MUSEUM_EXHIBIT_C2_Joker_1.wem
|
||||
Speech_MUSEUM_EXHIBIT_C2_Joker_HQ_1.wem
|
||||
Speech_MUSEUM_EXHIBIT_C2_Joker_Hyena_1.wem
|
||||
Speech_MUSEUM_EXHIBIT_C2_Penguin2_1.wem
|
||||
Speech_MUSEUM_EXHIBIT_C2_Penguin3_1.wem
|
||||
Speech_MUSEUM_EXHIBIT_C2_Victim_1.wem
|
||||
Speech_MUSEUM_EXHIBIT_C2_Victim_2.wem
|
||||
Speech_MUSEUM_S1_CH3_BOSS_PENG_rocketKILL_1.wem
|
||||
Speech_MUSEUM_S1_CH3_PENG_Enter_1.wem
|
||||
Speech_MUSEUM_S1_CH3_PENG_Enter_2.wem
|
||||
Speech_MUSEUM_S1_CH3_PENG_Enter_3.wem
|
||||
Speech_MUSEUM_S1_CH3_PENG_Enter_4.wem
|
||||
Speech_MUSEUM_S1_CH3_PENG_Enter_5.wem
|
||||
Speech_MUSEUM_S1_CH3_PENG_Firing_1_1.wem
|
||||
Speech_MUSEUM_S1_CH3_PENG_Firing_1_2.wem
|
||||
Speech_MUSEUM_S1_CH3_PENG_Firing_1_3.wem
|
||||
Speech_MUSEUM_S1_CH3_PENG_Firing_1_4.wem
|
||||
Speech_MUSEUM_S1_CH3_PENG_Firing_1_5.wem
|
||||
Speech_MUSEUM_S1_CH3_PENG_Firing_1_6.wem
|
||||
Speech_MUSEUM_S1_CH3_PENG_Firing_1_7.wem
|
||||
Speech_MUSEUM_S1_CH3_PENG_Firing_1_8.wem
|
||||
Speech_MUSEUM_S1_CH3_PENG_Firing_1_9.wem
|
||||
Speech_MUSEUM_S1_CH3_PENG_Firing_gadget_1.wem
|
||||
Speech_MUSEUM_S1_CH3_PENG_Firing_gadget_2.wem
|
||||
Speech_MUSEUM_S1_CH3_PENG_first_enter_1.wem
|
||||
Speech_MUSEUM_S1_CH3_PENG_first_enter_2.wem
|
||||
Speech_MUSEUM_S1_CH3_Peng_hits_BM_1.wem
|
||||
Speech_MUSEUM_S1_CH3_Peng_hits_BM_2.wem
|
||||
Speech_MUSEUM_S1_CH3_Peng_hits_BM_3.wem
|
||||
Speech_MUSEUM_S1_CH3_Peng_hits_BM_4.wem
|
||||
Speech_MUSEUM_S1_CH3_Peng_hits_BM_5.wem
|
||||
Speech_MUSEUM_S1_CH3_Peng_hits_BM_6.wem
|
||||
Speech_MUSEUM_S1_CH3_Peng_hits_BM_7.wem
|
||||
Speech_MUSEUM_S1_CH3_PENG_huh_1.wem
|
||||
Speech_MUSEUM_S1_CH3_PENG_huh_2.wem
|
||||
Speech_MUSEUM_S1_CH3_PENG_huh_3.wem
|
||||
Speech_MUSEUM_S1_CH3_PENG_huh_4.wem
|
||||
Speech_MUSEUM_S1_CH3_PENG_huh_intro_1.wem
|
||||
Speech_MUSEUM_S1_CH3_PENG_huh_intro_2.wem
|
||||
Speech_MUSEUM_S1_CH3_PENG_huh_intro_3.wem
|
||||
Speech_MUSEUM_S1_CH3_PENG_huh_intro_4.wem
|
||||
Speech_MUSEUM_S1_CH3_PENG_STOP_1.wem
|
||||
Speech_MUSEUM_S1_CH3_PENG_STOP_2.wem
|
||||
Speech_PRED_PENG_Automated_1.wem
|
||||
Speech_PRED_PENG_Automated_2.wem
|
||||
Speech_PRED_PENG_Automated_3.wem
|
||||
Speech_PRED_PENG_Automated_4.wem
|
||||
Speech_PRED_PENG_Automated_5.wem
|
||||
Speech_PRED_PENG_Automated_6.wem
|
||||
Speech_PRED_PENG_Automated_7.wem
|
||||
Speech_PRED_PENG_Automated_8.wem
|
||||
Speech_PRED_PENG_Automated_9.wem
|
||||
Speech_PRED_PENG_Automated_10.wem
|
||||
Speech_PRED_PENG_Automated_11.wem
|
||||
Speech_PRED_PENG_Automated_12.wem
|
||||
Speech_PRED_PENG_Automated_13.wem
|
||||
Speech_PRED_PENG_Automated_14.wem
|
||||
Speech_PRED_PENG_Automated_15.wem
|
||||
Speech_PRED_PENG_Automated_16.wem
|
||||
Speech_PRED_PENG_Automated_17.wem
|
||||
Speech_PRED_PENG_Automated_18.wem
|
||||
Speech_PRED_PENG_Automated_19.wem
|
||||
Speech_PRED_PENG_Automated_20.wem
|
||||
Speech_PRED_PENG_Automated_21.wem
|
||||
Speech_PRED_PENG_Automated_22.wem
|
||||
Speech_PRED_PENG_Automated_23.wem
|
||||
Speech_PRED_PENG_Automated_24.wem
|
||||
Speech_PRED_PENG_Automated_25.wem
|
||||
Speech_PRED_PENG_Automated_26.wem
|
||||
Speech_PRED_PENG_Automated_27.wem
|
||||
Speech_PRED_PENG_Automated_28.wem
|
||||
Speech_PRED_PENG_Automated_29.wem
|
||||
Speech_PRED_PENG_Automated_30.wem
|
||||
Speech_PRED_PENG_Automated_31.wem
|
||||
Speech_PRED_PENG_Automated_32.wem
|
||||
Speech_PRED_PENG_Automated_33.wem
|
||||
Speech_PRED_PENG_Automated_34.wem
|
||||
Speech_PRED_PENG_Automated_35.wem
|
||||
Speech_PRED_PENG_Automated_36.wem
|
||||
Speech_PRED_PENG_Automated_37.wem
|
||||
Speech_PRED_PENG_Automated_38.wem
|
||||
Speech_PRED_PENG_Automated_39.wem
|
||||
Speech_PRED_PENG_Automated_40.wem
|
||||
Speech_PRED_PENG_Automated_41.wem
|
||||
Speech_PRED_PENG_Automated_42.wem
|
||||
Speech_PRED_PENG_Automated_43.wem
|
||||
Speech_PRED_PENG_Automated_44.wem
|
||||
Speech_PRED_PENG_CH3_museum_ending_1.wem
|
||||
Speech_RADIO_SCAN_CH3_PENG_1Jammer_down_1.wem
|
||||
Speech_RADIO_SCAN_CH3_PENG_1Jammer_down_2.wem
|
||||
Speech_RADIO_SCAN_CH3_Peng_to_thugs_3.wem
|
||||
Speech_RADIO_SCAN_CH3_Peng_to_thugs_4.wem
|
||||
Speech_RADIO_SCAN_CH3_peng_to_thugs_and_BM_1.wem
|
||||
Speech_RADIO_SCAN_CH3_peng_to_thugs_and_BM_2.wem
|
||||
Speech_RADIO_SCAN_CH3_peng_to_thugs_and_BM_3.wem
|
||||
Speech_SEQUENCE_CH3_MUSEUM_B1_PENGUIN_VENOM_HENCH_1.wem
|
||||
Speech_SEQUENCE_CH3_MUSEUM_B1_PENGUIN_VENOM_HENCH_3.wem
|
||||
Speech_SEQUENCE_CH3_MUSEUM_B1_PENGUIN_VENOM_HENCH_4.wem
|
||||
Speech_SEQUENCE_CH3_MUSEUM_B2_PENGUIN_ICE_2.wem
|
||||
Speech_SEQUENCE_CH3_MUSEUM_B2_PENGUIN_ICE_3.wem
|
||||
Speech_SEQUENCE_CH3_MUSEUM_B2_PENGUIN_ICE_4.wem
|
||||
Speech_SEQUENCE_CH3_MUSEUM_B2_PENGUIN_ICE_5.wem
|
||||
Speech_SEQUENCE_CH3_MUSEUM_B2_PENGUIN_ICE_6.wem
|
||||
Speech_SEQUENCE_CH3_MUSEUM_B2_PENGUIN_ICE_8.wem
|
||||
Speech_SIDE_CAT_MEET_PENGUIN_3.wem
|
||||
Speech_SIDE_CAT_MEET_PENGUIN_4.wem
|
||||
Speech_STREETS_A2_CH3_Penguin_gloat_1.wem
|
||||
Speech_STREETS_A2_CH3_Penguin_gloat_2.wem
|
||||
Speech_STREETS_A2_CH3_Penguin_gloat_3.wem
|
||||
Speech_STREETS_A2_CH3_Penguin_gloat_4.wem
|
||||
Speech_STREETS_A2_CH3_Penguin_gloat_5.wem
|
||||
Speech_STREETS_A2_CH3_Penguin_gloat_6.wem
|
||||
Speech_STREETS_A2_CH3_Penguin_gloat_7.wem
|
||||
Speech_STREETS_A2_CH3_Penguin_gloat_8.wem
|
||||
Speech_STREETS_A5_CH3_PenguinTaunt_1.wem
|
||||
121
data/catalog/source/ita/Quinn.txt
Normal file
121
data/catalog/source/ita/Quinn.txt
Normal file
@@ -0,0 +1,121 @@
|
||||
Speech_CHAR_HARLEY_End_1.wem
|
||||
Speech_CHAR_HARLEY_End_2.wem
|
||||
Speech_CHAR_HARLEY_Harley_heads_1.wem
|
||||
Speech_CHAR_HARLEY_Harley_heads_2.wem
|
||||
Speech_CHAR_HARLEY_Harley_heads_3.wem
|
||||
Speech_CHAR_HARLEY_Harley_heads_4.wem
|
||||
Speech_CHAR_HARLEY_Harley_heads_5.wem
|
||||
Speech_CHAR_HARLEY_Harley_not_there_1.wem
|
||||
Speech_CHAR_HARLEY_Harley_not_there_2.wem
|
||||
Speech_CHAR_HARLEY_Insult_BM_1.wem
|
||||
Speech_CHAR_HARLEY_Insult_BM_2.wem
|
||||
Speech_CHAR_HARLEY_Insult_BM_3.wem
|
||||
Speech_CHAR_HARLEY_Insult_BM_4.wem
|
||||
Speech_CHAR_HARLEY_Insult_BM_5.wem
|
||||
Speech_CHAR_HARLEY_Insult_BM_6.wem
|
||||
Speech_CHAR_HARLEY_Insult_BM_7.wem
|
||||
Speech_CHAR_HARLEY_Insult_BM_8.wem
|
||||
Speech_CHAR_HARLEY_Insult_BM_9.wem
|
||||
Speech_CHAR_HARLEY_Insult_BM_10.wem
|
||||
Speech_CHAR_HARLEY_Insult_BM_11.wem
|
||||
Speech_CHURCH_B1_CH2_HQ_INTRO2_1.wem
|
||||
Speech_CHURCH_B1_CH2_HQ_INTRO2_2.wem
|
||||
Speech_CHURCH_B1_CH2_HQ_INTRO2_4.wem
|
||||
Speech_CHURCH_B1_CH2_HQ_INTRO2_5.wem
|
||||
Speech_CHURCH_B1_CH2_HQ_intro_1.wem
|
||||
Speech_CHURCH_B1_Ch2_HQ_scream_1.wem
|
||||
Speech_CINEMATIC_CH8_TALIA_KILLS_THE_WRONG_JOKER_28.wem
|
||||
Speech_GAME_OVER_Harley_1.wem
|
||||
Speech_GAME_OVER_Harley_2.wem
|
||||
Speech_GAME_OVER_Harley_3.wem
|
||||
Speech_GAME_OVER_Harley_4.wem
|
||||
Speech_GAME_OVER_Harley_5.wem
|
||||
Speech_PRED_HQ_Automated_1.wem
|
||||
Speech_PRED_HQ_Automated_2.wem
|
||||
Speech_PRED_HQ_Automated_3.wem
|
||||
Speech_PRED_HQ_Automated_4.wem
|
||||
Speech_PRED_HQ_Automated_5.wem
|
||||
Speech_PRED_HQ_Automated_6.wem
|
||||
Speech_PRED_HQ_Automated_7.wem
|
||||
Speech_PRED_HQ_Automated_8.wem
|
||||
Speech_PRED_HQ_Automated_9.wem
|
||||
Speech_PRED_HQ_Automated_10.wem
|
||||
Speech_PRED_HQ_Automated_11.wem
|
||||
Speech_PRED_HQ_Automated_12.wem
|
||||
Speech_PRED_HQ_Automated_13.wem
|
||||
Speech_PRED_HQ_Automated_14.wem
|
||||
Speech_PRED_HQ_Automated_15.wem
|
||||
Speech_PRED_HQ_Automated_16.wem
|
||||
Speech_PRED_HQ_Automated_17.wem
|
||||
Speech_PRED_HQ_Automated_18.wem
|
||||
Speech_PRED_HQ_Automated_19.wem
|
||||
Speech_PRED_HQ_Automated_20.wem
|
||||
Speech_PRED_HQ_Automated_21.wem
|
||||
Speech_PRED_HQ_Automated_22.wem
|
||||
Speech_PRED_HQ_Automated_23.wem
|
||||
Speech_PRED_HQ_Automated_24.wem
|
||||
Speech_PRED_HQ_Automated_25.wem
|
||||
Speech_PRED_HQ_Automated_26.wem
|
||||
Speech_PRED_HQ_Automated_27.wem
|
||||
Speech_PRED_HQ_Automated_28.wem
|
||||
Speech_PRED_HQ_Automated_29.wem
|
||||
Speech_PRED_HQ_Automated_30.wem
|
||||
Speech_PRED_HQ_Automated_31.wem
|
||||
Speech_PRED_HQ_Automated_32.wem
|
||||
Speech_PRED_HQ_Automated_33.wem
|
||||
Speech_PRED_HQ_Automated_34.wem
|
||||
Speech_PRED_HQ_Automated_35.wem
|
||||
Speech_PRED_HQ_Automated_36.wem
|
||||
Speech_PRED_HQ_Automated_37.wem
|
||||
Speech_PRED_HQ_CH2_End_1.wem
|
||||
Speech_PRED_J_CH5_GCPD_END_1.wem
|
||||
Speech_SEQUENCE_CH2_HARLEY_OFFICE_1.wem
|
||||
Speech_SEQUENCE_CH2_HARLEY_OFFICE_2.wem
|
||||
Speech_SEQUENCE_CH2_RESCUE_DOC_7.wem
|
||||
Speech_SEQUENCE_CH2_RESCUE_DOC_8.wem
|
||||
Speech_SEQUENCE_CH2_RESCUE_DOC_9.wem
|
||||
Speech_STEEL_A1_CH2_HQ_DoorWeld_1.wem
|
||||
Speech_STEEL_A1_CH2_HQ_DoorWeld_2.wem
|
||||
Speech_STEEL_B1_CH2_FinalFight_HQ_tears_1.wem
|
||||
Speech_STEEL_B1_CH2_Finalfight_HQ_tears_idles_1.wem
|
||||
Speech_STEEL_B1_CH2_Finalfight_HQ_tears_idles_2.wem
|
||||
Speech_STEEL_B1_CH2_Finalfight_HQ_tears_idles_3.wem
|
||||
Speech_STEEL_B1_CH2_HQ_Dismiss_Crowd_1.wem
|
||||
Speech_STEEL_B1_CH2_HQ_Dismiss_Crowd_2.wem
|
||||
Speech_STEEL_B1_CH2_HQ_Intro2_1.wem
|
||||
Speech_STEEL_B1_CH2_HQ_Intro2_2.wem
|
||||
Speech_STEEL_B1_CH2_HQ_Intro3_2.wem
|
||||
Speech_STEEL_B1_CH2_HQ_Intro_1.wem
|
||||
Speech_STEEL_B1_CH2_HQ_Intro_3.wem
|
||||
Speech_STEEL_B1_CH2_HQ_Lieu_Fight_1.wem
|
||||
Speech_STEEL_B1_CH2_HQ_Lieu_Fight_2.wem
|
||||
Speech_STEEL_B1_CH2_HQ_Lieu_Fight_3.wem
|
||||
Speech_STEEL_B1_CH2_HQ_Lieu_Fight_4.wem
|
||||
Speech_STEEL_B1_CH2_HQ_Muffled_Conversation_1.wem
|
||||
Speech_STEEL_B1_CH2_HQ_Stop_Execution_1.wem
|
||||
Speech_STEEL_B1_CH2_HQ_Stop_Execution_3.wem
|
||||
Speech_STEEL_B1_CH2_HQ_vis2_managersoffice_2.wem
|
||||
Speech_STEEL_B1_CH2_HQ_vis2_managersoffice_3.wem
|
||||
Speech_STEEL_B1_CH2_HQ_vis2_managersoffice_interrupt_1.wem
|
||||
Speech_STEEL_C2_CH6_harley_gag1_1.wem
|
||||
Speech_STEEL_C2_CH6_harley_gag_idle_1.wem
|
||||
Speech_STEEL_C2_CH6_harley_gag_idle_2.wem
|
||||
Speech_STEEL_C2_CH6_harley_gag_idle_3.wem
|
||||
Speech_STEEL_C2_CH6_harley_gag_idle_4.wem
|
||||
Speech_STEEL_C2_CH6_harley_gag_idle_5.wem
|
||||
Speech_STEEL_C2_CH6_harley_gag_idle_6.wem
|
||||
Speech_STEEL_C2_CH6_harley_gag_idle_7.wem
|
||||
Speech_STEEL_C2_CH6_Harleyregag_1.wem
|
||||
Speech_STEEL_C2_CH6_Harleyregag_2.wem
|
||||
Speech_STEEL_C2_CH6_harleyungagged_1.wem
|
||||
Speech_STEEL_C2_CH6_harleyungagged_2.wem
|
||||
Speech_STEEL_C2_CH6_harleyungagged_3.wem
|
||||
Speech_STEEL_C2_CH6_HQ_end_gag_1.wem
|
||||
Speech_STEEL_C3_CH3_HQ_Power_Off_3.wem
|
||||
Speech_STEEL_C3_CH3_HQ_Power_Off_4.wem
|
||||
Speech_STEEL_C3_CH3_HQ_Power_Off_5.wem
|
||||
Speech_ZONES_STREET_C_ROAD5_CH2_HQ_PA_INTRO_1.wem
|
||||
Speech_ZONES_STREET_C_ROAD5_CH2_HQ_PA_talks_to_thugs_1.wem
|
||||
Speech_ZONES_STREET_C_ROAD5_CH2_HQ_PA_talks_to_thugs_2.wem
|
||||
Speech_ZONES_STREET_C_ROAD5_CH2_HQ_PA_talks_to_thugs_3.wem
|
||||
Speech_ZONES_STREET_C_ROAD5_CH2_HQ_PA_talks_to_thugs_4.wem
|
||||
100
data/catalog/source/ita/Ras.txt
Normal file
100
data/catalog/source/ita/Ras.txt
Normal file
@@ -0,0 +1,100 @@
|
||||
Speech_CHAR_RAS_Demon_trials_1.wem
|
||||
Speech_CHAR_RAS_Demon_trials_2.wem
|
||||
Speech_CHAR_RAS_Demon_trials_3.wem
|
||||
Speech_CHAR_RAS_Demon_trials_4.wem
|
||||
Speech_CHAR_RAS_Demon_trials_5.wem
|
||||
Speech_CHAR_RAS_Demon_trials_6.wem
|
||||
Speech_CHAR_RAS_Demon_trials_7.wem
|
||||
Speech_CHAR_RAS_Demon_trials_8.wem
|
||||
Speech_CHAR_RAS_Demon_trials_9.wem
|
||||
Speech_CHAR_RAS_Laugh_1.wem
|
||||
Speech_CHAR_RAS_No_detective_1.wem
|
||||
Speech_CHAR_RAS_No_detective_2.wem
|
||||
Speech_CHAR_RAS_No_detective_3.wem
|
||||
Speech_GAME_OVER_Ras_al_Ghul_1.wem
|
||||
Speech_GAME_OVER_Ras_al_Ghul_2.wem
|
||||
Speech_GAME_OVER_Ras_al_Ghul_3.wem
|
||||
Speech_GAME_OVER_Ras_al_Ghul_4.wem
|
||||
Speech_GAME_OVER_Ras_al_Ghul_5.wem
|
||||
Speech_UNDER_C2_CH4_Wonder1_1.wem
|
||||
Speech_UNDER_C2_CH4_Wonder1_2.wem
|
||||
Speech_UNDER_C2_CH4_Wonder1_3.wem
|
||||
Speech_UNDER_C2_CH4_Wonder1_4.wem
|
||||
Speech_UNDER_C2_CH4_Wonder1_5.wem
|
||||
Speech_UNDER_C2_CH4_Wonder2_1.wem
|
||||
Speech_UNDER_C2_CH4_Wonder2_2.wem
|
||||
Speech_UNDER_C2_CH4_Wonder2_3.wem
|
||||
Speech_UNDER_C2_CH4_Wonder3_1.wem
|
||||
Speech_UNDER_C2_CH4_Wonder3_2.wem
|
||||
Speech_UNDER_C2_CH4_Wonder3_3.wem
|
||||
Speech_UNDER_C2_CH4_Wonder3_4.wem
|
||||
Speech_UNDER_S1_Army_of_Ras_1.wem
|
||||
Speech_UNDER_S1_Army_of_Ras_2.wem
|
||||
Speech_UNDER_S1_Army_of_Ras_3.wem
|
||||
Speech_UNDER_S1_Army_of_Ras_4.wem
|
||||
Speech_UNDER_S1_Army_of_Ras_5.wem
|
||||
Speech_UNDER_S1_Army_of_Ras_6.wem
|
||||
Speech_UNDER_S1_Army_of_Ras_7.wem
|
||||
Speech_UNDER_S1_Army_of_Ras_8.wem
|
||||
Speech_UNDER_S1_Army_of_Ras_9.wem
|
||||
Speech_UNDER_S1_Army_of_Ras_10.wem
|
||||
Speech_UNDER_S1_Army_of_Ras_11.wem
|
||||
Speech_UNDER_S1_Army_of_Ras_12.wem
|
||||
Speech_UNDER_S1_Boss_conv2_1.wem
|
||||
Speech_UNDER_S1_Boss_conv2_4.wem
|
||||
Speech_UNDER_S1_Boss_intro_1.wem
|
||||
Speech_UNDER_S1_EMOTE_1.wem
|
||||
Speech_UNDER_S1_EMOTE_2.wem
|
||||
Speech_UNDER_S1_EMOTE_3.wem
|
||||
Speech_UNDER_S1_EMOTE_4.wem
|
||||
Speech_UNDER_S1_EMOTE_5.wem
|
||||
Speech_UNDER_S1_EMOTE_6.wem
|
||||
Speech_UNDER_S1_EMOTE_7.wem
|
||||
Speech_UNDER_S1_EMOTE_8.wem
|
||||
Speech_UNDER_S1_EMOTE_9.wem
|
||||
Speech_UNDER_S1_EMOTE_10.wem
|
||||
Speech_UNDER_S1_EMOTE_11.wem
|
||||
Speech_UNDER_S1_EMOTE_12.wem
|
||||
Speech_UNDER_S1_EMOTE_13.wem
|
||||
Speech_UNDER_S1_EMOTE_14.wem
|
||||
Speech_UNDER_S1_EMOTE_15.wem
|
||||
Speech_UNDER_S1_EMOTE_16.wem
|
||||
Speech_UNDER_S1_EMOTE_17.wem
|
||||
Speech_UNDER_S1_EMOTE_18.wem
|
||||
Speech_UNDER_S1_EMOTE_19.wem
|
||||
Speech_UNDER_S1_EMOTE_20.wem
|
||||
Speech_UNDER_S1_EMOTE_21.wem
|
||||
Speech_UNDER_S1_EMOTE_22.wem
|
||||
Speech_UNDER_S1_EMOTE_23.wem
|
||||
Speech_UNDER_S1_EMOTE_24.wem
|
||||
Speech_UNDER_S1_EMOTE_25.wem
|
||||
Speech_UNDER_S1_EMOTE_26.wem
|
||||
Speech_UNDER_S1_EMOTE_27.wem
|
||||
Speech_UNDER_S1_EMOTE_28.wem
|
||||
Speech_UNDER_S1_EMOTE_29.wem
|
||||
Speech_UNDER_S1_EMOTE_30.wem
|
||||
Speech_UNDER_S1_EMOTE_31.wem
|
||||
Speech_UNDER_S1_EMOTE_Hit_light_1.wem
|
||||
Speech_UNDER_S1_EMOTE_Hit_light_2.wem
|
||||
Speech_UNDER_S1_EMOTE_Hit_light_3.wem
|
||||
Speech_UNDER_S1_EMOTE_Hit_light_4.wem
|
||||
Speech_UNDER_S1_EMOTE_Hit_light_5.wem
|
||||
Speech_UNDER_S1_EMOTE_Hit_light_6.wem
|
||||
Speech_UNDER_S1_EMOTE_Hit_light_7.wem
|
||||
Speech_UNDER_S1_EMOTE_Hit_light_8.wem
|
||||
Speech_UNDER_S1_EMOTE_Hit_light_9.wem
|
||||
Speech_UNDER_S1_EMOTE_Hit_light_10.wem
|
||||
Speech_UNDER_S1_GAINT_RAS_Intro_1.wem
|
||||
Speech_UNDER_S1_Giant_Ras_laugh_1.wem
|
||||
Speech_UNDER_S1_Giant_Ras_laugh_2.wem
|
||||
Speech_UNDER_S1_Giant_Ras_laugh_3.wem
|
||||
Speech_UNDER_S1_Giant_Ras_laugh_4.wem
|
||||
Speech_UNDER_S1_Giant_Ras_laugh_5.wem
|
||||
Speech_UNDER_S1_Giant_Ras_laugh_6.wem
|
||||
Speech_UNDER_S1_Giant_Ras_laugh_7.wem
|
||||
Speech_UNDER_S1_Giant_Ras_laugh_8.wem
|
||||
Speech_UNDER_S1_Giant_Ras_taunt1_1.wem
|
||||
Speech_UNDER_S1_Giant_Ras_taunt2_1.wem
|
||||
Speech_UNDER_S1_Post_electric_sky_attack_1.wem
|
||||
Speech_UNDER_S1_Ras_Trials_Complete_1.wem
|
||||
Speech_UNDER_S1_SwordStruggle1_1.wem
|
||||
353
data/catalog/source/ita/Riddler.txt
Normal file
353
data/catalog/source/ita/Riddler.txt
Normal file
@@ -0,0 +1,353 @@
|
||||
CHR_RadioScanner_5.wem
|
||||
CHR_RadioScanner_19.wem
|
||||
CHR_RadioScanner_42.wem
|
||||
CHR_RadioScanner_43.wem
|
||||
CHR_RadioScanner_45.wem
|
||||
CHR_RadioScanner_48.wem
|
||||
CHR_RadioScanner_81.wem
|
||||
CHR_RadioScanner_89.wem
|
||||
Speech_CHURCH_B1_Informant_outside_1.wem
|
||||
Speech_CHURCH_B1_RIDDLER_FIRST_RIDDLE_INTRO_1.wem
|
||||
Speech_CHURCH_B1_RIDDLER_FIRST_RIDDLE_INTRO_2.wem
|
||||
Speech_CHURCH_B1_RIDDLER_FIRST_RIDDLE_INTRO_4.wem
|
||||
Speech_CHURCH_B1_RIDDLER_FIRST_RIDDLE_INTRO_5.wem
|
||||
Speech_CHURCH_B1_RIDDLER_FIRST_RIDDLE_PROMPTS_1.wem
|
||||
Speech_CHURCH_B1_RIDDLER_FIRST_RIDDLE_PROMPTS_2.wem
|
||||
Speech_CHURCH_B1_RIDDLER_FIRST_RIDDLE_PROMPTS_3.wem
|
||||
Speech_CHURCH_B1_RIDDLER_FIRST_RIDDLE_PROMPTS_4.wem
|
||||
Speech_GAME_OVER_Riddler_1.wem
|
||||
Speech_GAME_OVER_Riddler_2.wem
|
||||
Speech_GAME_OVER_Riddler_3.wem
|
||||
Speech_GAME_OVER_Riddler_4.wem
|
||||
Speech_GAME_OVER_Riddler_5.wem
|
||||
Speech_GAME_OVER_Riddler_6.wem
|
||||
Speech_GAME_OVER_Riddler_7.wem
|
||||
Speech_GAME_OVER_Riddler_8.wem
|
||||
Speech_GAME_OVER_Riddler_9.wem
|
||||
Speech_GAME_OVER_Riddler_10.wem
|
||||
Speech_GAME_OVER_Riddler_11.wem
|
||||
Speech_GAME_OVER_Riddler_12.wem
|
||||
Speech_PRED_RIDDLER_Automated_1.wem
|
||||
Speech_PRED_RIDDLER_Automated_2.wem
|
||||
Speech_PRED_RIDDLER_Automated_3.wem
|
||||
Speech_PRED_RIDDLER_Automated_4.wem
|
||||
Speech_PRED_RIDDLER_Automated_5.wem
|
||||
Speech_PRED_RIDDLER_Automated_6.wem
|
||||
Speech_PRED_RIDDLER_Automated_7.wem
|
||||
Speech_PRED_RIDDLER_Automated_8.wem
|
||||
Speech_PRED_RIDDLER_Automated_9.wem
|
||||
Speech_PRED_RIDDLER_Automated_10.wem
|
||||
Speech_PRED_RIDDLER_Automated_11.wem
|
||||
Speech_PRED_RIDDLER_Automated_12.wem
|
||||
Speech_PRED_RIDDLER_Automated_13.wem
|
||||
Speech_PRED_RIDDLER_Automated_14.wem
|
||||
Speech_PRED_RIDDLER_Automated_15.wem
|
||||
Speech_PRED_RIDDLER_Automated_16.wem
|
||||
Speech_PRED_RIDDLER_Automated_17.wem
|
||||
Speech_PRED_RIDDLER_Automated_18.wem
|
||||
Speech_PRED_RIDDLER_Automated_19.wem
|
||||
Speech_PRED_RIDDLER_Automated_20.wem
|
||||
Speech_PRED_RIDDLER_Automated_21.wem
|
||||
Speech_PRED_RIDDLER_Automated_22.wem
|
||||
Speech_PRED_RIDDLER_Automated_23.wem
|
||||
Speech_PRED_RIDDLER_Automated_24.wem
|
||||
Speech_PRED_RIDDLER_Automated_25.wem
|
||||
Speech_PRED_RIDDLER_Automated_26.wem
|
||||
Speech_PRED_RIDDLER_Automated_27.wem
|
||||
Speech_PRED_RIDDLER_Automated_28.wem
|
||||
Speech_PRED_RIDDLER_Automated_29.wem
|
||||
Speech_PRED_RIDDLER_Automated_30.wem
|
||||
Speech_PRED_RIDDLER_Automated_31.wem
|
||||
Speech_PRED_RIDDLER_Automated_32.wem
|
||||
Speech_PRED_RIDDLER_Automated_33.wem
|
||||
Speech_PRED_RIDDLER_Automated_34.wem
|
||||
Speech_PRED_RIDDLER_Automated_35.wem
|
||||
Speech_PRED_RIDDLER_Automated_36.wem
|
||||
Speech_PRED_RIDDLER_Automated_37.wem
|
||||
Speech_PRED_RIDDLER_Automated_38.wem
|
||||
Speech_PRED_RIDDLER_Automated_39.wem
|
||||
Speech_PRED_RIDDLER_Automated_40.wem
|
||||
Speech_PRED_RIDDLER_Automated_41.wem
|
||||
Speech_PRED_RIDDLER_Automated_42.wem
|
||||
Speech_PRED_RIDDLER_Automated_43.wem
|
||||
Speech_PRED_RIDDLER_Automated_44.wem
|
||||
Speech_PRED_RIDDLER_Automated_45.wem
|
||||
Speech_PRED_RIDDLER_Automated_46.wem
|
||||
Speech_PRED_RIDDLER_Automated_47.wem
|
||||
Speech_PRED_RIDDLER_Automated_48.wem
|
||||
Speech_PRED_RIDDLER_Automated_49.wem
|
||||
Speech_PRED_RIDDLER_Automated_50.wem
|
||||
Speech_PRED_RIDDLER_Automated_51.wem
|
||||
Speech_PRED_RIDDLER_Automated_52.wem
|
||||
Speech_PRED_RIDDLER_Automated_53.wem
|
||||
Speech_PRED_RIDDLER_Automated_54.wem
|
||||
Speech_PRED_RIDDLER_Automated_55.wem
|
||||
Speech_PRED_RIDDLER_Automated_56.wem
|
||||
Speech_PRED_RIDDLER_Automated_57.wem
|
||||
Speech_PRED_RIDDLER_Automated_58.wem
|
||||
Speech_PRED_RIDDLER_Automated_59.wem
|
||||
Speech_PRED_RIDDLER_Automated_60.wem
|
||||
Speech_PRED_RIDDLER_Automated_61.wem
|
||||
Speech_PRED_RIDDLER_Automated_62.wem
|
||||
Speech_PRED_RIDDLER_Automated_63.wem
|
||||
Speech_PRED_RIDDLER_Automated_64.wem
|
||||
Speech_PRED_RIDDLER_Automated_65.wem
|
||||
Speech_PRED_RIDDLER_Automated_66.wem
|
||||
Speech_PRED_RIDDLER_Automated_67.wem
|
||||
Speech_PRED_RIDDLER_Automated_68.wem
|
||||
Speech_PRED_RIDDLER_Automated_69.wem
|
||||
Speech_PRED_RIDDLER_Automated_70.wem
|
||||
Speech_PRED_RIDDLER_Automated_71.wem
|
||||
Speech_PRED_RIDDLER_Automated_72.wem
|
||||
Speech_PRED_RIDDLER_Automated_73.wem
|
||||
Speech_PRED_RIDDLER_Automated_74.wem
|
||||
Speech_PRED_RIDDLER_Automated_75.wem
|
||||
Speech_RIDDLER_Enigma_Cat_trophy_1.wem
|
||||
Speech_RIDDLER_Enigma_Cat_trophy_2.wem
|
||||
Speech_RIDDLER_Enigma_Cat_trophy_3.wem
|
||||
Speech_RIDDLER_Enigma_Control_box_goto_church_1.wem
|
||||
Speech_RIDDLER_Enigma_Control_box_goto_church_2.wem
|
||||
Speech_RIDDLER_Enigma_Control_box_goto_church_3.wem
|
||||
Speech_RIDDLER_Enigma_Control_box_too_soon_1.wem
|
||||
Speech_RIDDLER_Enigma_Control_box_too_soon_2.wem
|
||||
Speech_RIDDLER_Enigma_Control_box_too_soon_3.wem
|
||||
Speech_RIDDLER_Enigma_Fail_1.wem
|
||||
Speech_RIDDLER_Enigma_Fail_1_1.wem
|
||||
Speech_RIDDLER_Enigma_Fail_2.wem
|
||||
Speech_RIDDLER_Enigma_Fail_3.wem
|
||||
Speech_RIDDLER_Enigma_Fail_4.wem
|
||||
Speech_RIDDLER_Enigma_Fail_5.wem
|
||||
Speech_RIDDLER_Enigma_Fail_6.wem
|
||||
Speech_RIDDLER_Enigma_Fail_7.wem
|
||||
Speech_RIDDLER_Enigma_Fail_8.wem
|
||||
Speech_RIDDLER_Enigma_Fail_9.wem
|
||||
Speech_RIDDLER_Enigma_Fail_10.wem
|
||||
Speech_RIDDLER_Enigma_Fail_11.wem
|
||||
Speech_RIDDLER_Enigma_Fail_12.wem
|
||||
Speech_RIDDLER_Enigma_Fail_13.wem
|
||||
Speech_RIDDLER_Enigma_Fail_first_1.wem
|
||||
Speech_RIDDLER_Enigma_Fail_first_2.wem
|
||||
Speech_RIDDLER_Enigma_Fail_first_3.wem
|
||||
Speech_RIDDLER_Enigma_Fail_first_4.wem
|
||||
Speech_RIDDLER_Enigma_Fail_first_5.wem
|
||||
Speech_RIDDLER_Enigma_Fail_first_6.wem
|
||||
Speech_RIDDLER_Enigma_Fail_first_7.wem
|
||||
Speech_RIDDLER_Enigma_Fail_first_playonce_1.wem
|
||||
Speech_RIDDLER_Enigma_Fail_second_playonce_1.wem
|
||||
Speech_RIDDLER_Enigma_Fail_second_playonce_2.wem
|
||||
Speech_RIDDLER_Enigma_Got_enough_trophies_1.wem
|
||||
Speech_RIDDLER_Enigma_Got_enough_trophies_2.wem
|
||||
Speech_RIDDLER_Enigma_Got_enough_trophies_3.wem
|
||||
Speech_RIDDLER_Enigma_Got_enough_trophies_4.wem
|
||||
Speech_RIDDLER_Enigma_Got_enough_trophies_5.wem
|
||||
Speech_RIDDLER_Enigma_Hostage_Riddles_1.wem
|
||||
Speech_RIDDLER_Enigma_Hostage_Riddles_2.wem
|
||||
Speech_RIDDLER_Enigma_Hostage_Riddles_3.wem
|
||||
Speech_RIDDLER_Enigma_Hostage_Riddles_4.wem
|
||||
Speech_RIDDLER_Enigma_Hostage_Riddles_5.wem
|
||||
Speech_RIDDLER_Enigma_Hostage_taunt_1.wem
|
||||
Speech_RIDDLER_Enigma_Hostage_taunt_2.wem
|
||||
Speech_RIDDLER_Enigma_Hostage_taunt_3.wem
|
||||
Speech_RIDDLER_Enigma_Hostage_taunt_4.wem
|
||||
Speech_RIDDLER_Enigma_Hostage_taunt_5.wem
|
||||
Speech_RIDDLER_Enigma_Map_explain_rule_1.wem
|
||||
Speech_RIDDLER_Enigma_No_go_first_room_remind_1.wem
|
||||
Speech_RIDDLER_Enigma_No_go_first_room_remind_2.wem
|
||||
Speech_RIDDLER_Enigma_No_go_first_room_remind_3.wem
|
||||
Speech_RIDDLER_Enigma_No_go_first_room_remind_4.wem
|
||||
Speech_RIDDLER_Enigma_Not_enough_1.wem
|
||||
Speech_RIDDLER_Enigma_Not_enough_2.wem
|
||||
Speech_RIDDLER_Enigma_Not_enough_3.wem
|
||||
Speech_RIDDLER_Enigma_Solved_1_1.wem
|
||||
Speech_RIDDLER_Enigma_Solved_2_1.wem
|
||||
Speech_RIDDLER_Enigma_Solved_3_1.wem
|
||||
Speech_RIDDLER_Enigma_Solved_4_1.wem
|
||||
Speech_RIDDLER_Enigma_Solved_5_1.wem
|
||||
Speech_RIDDLER_Keith_1.wem
|
||||
Speech_RIDDLER_Keith_2.wem
|
||||
Speech_RIDDLER_Poz_rec_temp_1.wem
|
||||
Speech_RIDDLER_Poz_rec_temp_2.wem
|
||||
Speech_RIDDLER_Poz_rec_temp_3.wem
|
||||
Speech_RIDDLER_Poz_rec_temp_4.wem
|
||||
Speech_RIDDLER_Poz_rec_temp_5.wem
|
||||
Speech_RIDDLER_R1_BM_enter_room_1.wem
|
||||
Speech_RIDDLER_R1_Fail_door_1.wem
|
||||
Speech_RIDDLER_R1_Fail_door_2.wem
|
||||
Speech_RIDDLER_R1_Hit_switch1_1.wem
|
||||
Speech_RIDDLER_R1_Hit_switch2_1.wem
|
||||
Speech_RIDDLER_R1_IDLES_1.wem
|
||||
Speech_RIDDLER_R1_IDLES_2.wem
|
||||
Speech_RIDDLER_R1_IDLES_3.wem
|
||||
Speech_RIDDLER_R1_IDLES_4.wem
|
||||
Speech_RIDDLER_R1_intro_2.wem
|
||||
Speech_RIDDLER_R1_intro_3.wem
|
||||
Speech_RIDDLER_R1_intro_4.wem
|
||||
Speech_RIDDLER_R1_intro_5.wem
|
||||
Speech_RIDDLER_R1_post_complete_taunt_1.wem
|
||||
Speech_RIDDLER_R1_Shocking_BM_1.wem
|
||||
Speech_RIDDLER_R1_Shocking_BM_2.wem
|
||||
Speech_RIDDLER_R1_switch_taunts_1.wem
|
||||
Speech_RIDDLER_R1_switch_taunts_2.wem
|
||||
Speech_RIDDLER_R1_switch_taunts_3.wem
|
||||
Speech_RIDDLER_R2_hit_correct_switch_1.wem
|
||||
Speech_RIDDLER_R2_intro_1.wem
|
||||
Speech_RIDDLER_R2_intro_2.wem
|
||||
Speech_RIDDLER_R2_intro_3.wem
|
||||
Speech_RIDDLER_R2_intro_4.wem
|
||||
Speech_RIDDLER_R2_post_complete_taunt_1.wem
|
||||
Speech_RIDDLER_R2_Spin_idles_1.wem
|
||||
Speech_RIDDLER_R2_Spin_idles_2.wem
|
||||
Speech_RIDDLER_R2_wait_too_long_1.wem
|
||||
Speech_RIDDLER_R2_wait_too_long_2.wem
|
||||
Speech_RIDDLER_R2_wait_too_long_3.wem
|
||||
Speech_RIDDLER_R2_wrong_1_riddler_1.wem
|
||||
Speech_RIDDLER_R2_wrong_1_riddler_2.wem
|
||||
Speech_RIDDLER_R3_new_Intro_1.wem
|
||||
Speech_RIDDLER_R3_new_Intro_2.wem
|
||||
Speech_RIDDLER_R3_new_Outro_1.wem
|
||||
Speech_RIDDLER_R3_new_pressure_1.wem
|
||||
Speech_RIDDLER_R3_new_pressure_2.wem
|
||||
Speech_RIDDLER_R3_new_pressure_3.wem
|
||||
Speech_RIDDLER_R3_new_use_ice_1.wem
|
||||
Speech_RIDDLER_R4_IDLES_1.wem
|
||||
Speech_RIDDLER_R4_IDLES_2.wem
|
||||
Speech_RIDDLER_R4_intro_1.wem
|
||||
Speech_RIDDLER_R4_intro_2.wem
|
||||
Speech_RIDDLER_R4_intro_3.wem
|
||||
Speech_RIDDLER_R4_new_outro_1.wem
|
||||
Speech_RIDDLER_R4_Scared_idle_1.wem
|
||||
Speech_RIDDLER_R4_Scared_idle_2.wem
|
||||
Speech_RIDDLER_R5_new_BM_shocked_1.wem
|
||||
Speech_RIDDLER_R5_new_BM_shocked_2.wem
|
||||
Speech_RIDDLER_R5_new_Cannon_Smash_Wall_1.wem
|
||||
Speech_RIDDLER_R5_new_Cannon_Torment_1.wem
|
||||
Speech_RIDDLER_R5_new_Cannon_Torment_2.wem
|
||||
Speech_RIDDLER_R5_new_Cannon_Torment_3.wem
|
||||
Speech_RIDDLER_R5_new_Hostage_intro_1.wem
|
||||
Speech_RIDDLER_R5_new_IDLES_1.wem
|
||||
Speech_RIDDLER_R5_new_IDLES_2.wem
|
||||
Speech_RIDDLER_R5_new_IDLES_3.wem
|
||||
Speech_RIDDLER_R5_new_IDLES_4.wem
|
||||
Speech_RIDDLER_R5_new_IDLES_5.wem
|
||||
Speech_RIDDLER_R5_new_Intro_1.wem
|
||||
Speech_RIDDLER_R5_new_Intro_2.wem
|
||||
Speech_RIDDLER_R5_new_Intro_3.wem
|
||||
Speech_RIDDLER_R5_new_Shooting_Range_Intro_1.wem
|
||||
Speech_RIDDLER_R5_new_Wait_For_BM_Solve_Muncher_1.wem
|
||||
Speech_RIDDLER_R5_new_wait_too_long_1.wem
|
||||
Speech_RIDDLER_R5_new_wait_too_long_2.wem
|
||||
Speech_RIDDLER_R5_new_wait_too_long_3.wem
|
||||
Speech_RIDDLER_R5_outro_1.wem
|
||||
Speech_RIDDLER_R5_platform_descend_1.wem
|
||||
Speech_RIDDLER_R6_buttonloops_1.wem
|
||||
Speech_RIDDLER_R6_buttonloops_2.wem
|
||||
Speech_RIDDLER_R6_buttonloops_3.wem
|
||||
Speech_RIDDLER_R6_buttonloops_4.wem
|
||||
Speech_RIDDLER_R6_buttonloops_5.wem
|
||||
Speech_RIDDLER_R6_Intro_1.wem
|
||||
Speech_RIDDLER_R6_Intro_2.wem
|
||||
Speech_RIDDLER_R6_Intro_3.wem
|
||||
Speech_RIDDLER_R6_Intro_7.wem
|
||||
Speech_RIDDLER_R6_Riddler_blocking_1.wem
|
||||
Speech_RIDDLER_R6_Riddler_blocking_2.wem
|
||||
Speech_RIDDLER_R6_Riddler_blocking_3.wem
|
||||
Speech_RIDDLER_R6_Riddler_blocking_4.wem
|
||||
Speech_RIDDLER_R6_Riddler_button_1.wem
|
||||
Speech_RIDDLER_R6_Riddler_button_2.wem
|
||||
Speech_RIDDLER_R6_Riddler_button_3.wem
|
||||
Speech_RIDDLER_R6_Riddler_button_4.wem
|
||||
Speech_RIDDLER_R6_Riddler_button_5.wem
|
||||
Speech_RIDDLER_R6_Riddler_button_6.wem
|
||||
Speech_RIDDLER_R6_Riddler_idles_chain_1.wem
|
||||
Speech_RIDDLER_R6_Riddler_idles_chain_2.wem
|
||||
Speech_RIDDLER_R6_Riddler_idles_chain_3.wem
|
||||
Speech_RIDDLER_R6_Riddler_idles_chain_4.wem
|
||||
Speech_RIDDLER_R6_Riddler_idles_chain_5.wem
|
||||
Speech_RIDDLER_R6_Riddler_idles_chain_6.wem
|
||||
Speech_RIDDLER_R6_Riddler_idles_chain_7.wem
|
||||
Speech_RIDDLER_R6_Riddler_mumbles_1.wem
|
||||
Speech_RIDDLER_R6_Riddler_mumbles_2.wem
|
||||
Speech_RIDDLER_R6_Riddler_mumbles_3.wem
|
||||
Speech_RIDDLER_R6_Riddler_mumbles_4.wem
|
||||
Speech_RIDDLER_R6_Riddler_mumbles_5.wem
|
||||
Speech_RIDDLER_R6_Riddler_mumbles_6.wem
|
||||
Speech_RIDDLER_R_Count_down2_1.wem
|
||||
Speech_RIDDLER_R_Count_down2_2.wem
|
||||
Speech_RIDDLER_R_Count_down2_3.wem
|
||||
Speech_RIDDLER_R_Count_down2_4.wem
|
||||
Speech_RIDDLER_R_Count_down_1.wem
|
||||
Speech_RIDDLER_R_Count_down_2.wem
|
||||
Speech_RIDDLER_R_Count_down_3.wem
|
||||
Speech_RIDDLER_R_Count_down_4.wem
|
||||
Speech_RIDDLER_R_Switch_1.wem
|
||||
Speech_RIDDLER_R_Switch_2.wem
|
||||
Speech_RIDDLER_R_Switch_3.wem
|
||||
Speech_RIDDLER_R_Tick_1.wem
|
||||
Speech_RIDDLER_R_Tick_2.wem
|
||||
Speech_RIDDLER_Secrets_Found_first_1.wem
|
||||
Speech_RIDDLES_CAMEO_DONOTUSE_1.wem
|
||||
Speech_RIDDLES_CAMEO_DONOTUSE_2.wem
|
||||
Speech_RIDDLES_CAMEO_DONOTUSE_3.wem
|
||||
Speech_RIDDLES_CAMEO_DONOTUSE_4.wem
|
||||
Speech_RIDDLES_CAMEO_DONOTUSE_5.wem
|
||||
Speech_RIDDLES_CAMEO_DONOTUSE_6.wem
|
||||
Speech_RIDDLES_CAMEO_DONOTUSE_7.wem
|
||||
Speech_RIDDLES_CAMEO_DONOTUSE_8.wem
|
||||
Speech_RIDDLES_CAMEO_DONOTUSE_9.wem
|
||||
Speech_RIDDLES_CAMEO_DONOTUSE_10.wem
|
||||
Speech_RIDDLES_CAMEO_DONOTUSE_11.wem
|
||||
Speech_RIDDLES_CAMEO_DONOTUSE_12.wem
|
||||
Speech_RIDDLES_CAMEO_DONOTUSE_13.wem
|
||||
Speech_RIDDLES_CAMEO_DONOTUSE_14.wem
|
||||
Speech_RIDDLES_CAMEO_DONOTUSE_15.wem
|
||||
Speech_RIDDLES_CAMEO_DONOTUSE_16.wem
|
||||
Speech_RIDDLES_CAMEO_DONOTUSE_17.wem
|
||||
Speech_RIDDLES_CAMEO_DONOTUSE_18.wem
|
||||
Speech_RIDDLES_CAMEO_DONOTUSE_19.wem
|
||||
Speech_RIDDLES_CAMEO_DONOTUSE_20.wem
|
||||
Speech_RIDDLES_CAMEO_DONOTUSE_21.wem
|
||||
Speech_RIDDLES_CAMEO_DONOTUSE_22.wem
|
||||
Speech_RIDDLES_CAMEO_DONOTUSE_23.wem
|
||||
Speech_RIDDLES_CAMEO_DONOTUSE_24.wem
|
||||
Speech_RIDDLES_CAMEO_DONOTUSE_25.wem
|
||||
Speech_RIDDLES_CAMEO_DONOTUSE_26.wem
|
||||
Speech_RIDDLES_CAMEO_DONOTUSE_27.wem
|
||||
Speech_RIDDLES_CAMEO_DONOTUSE_28.wem
|
||||
Speech_RIDDLES_CAMEO_DONOTUSE_29.wem
|
||||
Speech_RIDDLES_CAMEO_DONOTUSE_30.wem
|
||||
Speech_RIDDLES_CAMEO_DONOTUSE_31.wem
|
||||
Speech_RIDDLES_CAMEO_DONOTUSE_32.wem
|
||||
Speech_RIDDLES_CAMEO_DONOTUSE_33.wem
|
||||
Speech_RIDDLES_CAMEO_DONOTUSE_34.wem
|
||||
Speech_RIDDLES_CAMEO_DONOTUSE_35.wem
|
||||
Speech_RIDDLES_CAMEO_DONOTUSE_36.wem
|
||||
Speech_RIDDLES_CAMEO_DONOTUSE_37.wem
|
||||
Speech_RIDDLES_CAMEO_DONOTUSE_38.wem
|
||||
Speech_RIDDLES_CAMEO_DONOTUSE_39.wem
|
||||
Speech_RIDDLES_CAMEO_DONOTUSE_40.wem
|
||||
Speech_RIDDLES_CAMEO_DONOTUSE_41.wem
|
||||
Speech_RIDDLES_CAMEO_DONOTUSE_42.wem
|
||||
Speech_RIDDLES_CAMEO_DONOTUSE_43.wem
|
||||
Speech_RIDDLES_CAMEO_DONOTUSE_44.wem
|
||||
Speech_RIDDLES_CAMEO_DONOTUSE_45.wem
|
||||
Speech_RIDDLES_CAMEO_DONOTUSE_46.wem
|
||||
Speech_RIDDLES_CAMEO_DONOTUSE_47.wem
|
||||
Speech_RIDDLES_CAMEO_DONOTUSE_48.wem
|
||||
Speech_RIDDLES_CAMEO_DONOTUSE_49.wem
|
||||
Speech_RIDDLES_CAMEO_DONOTUSE_50.wem
|
||||
Speech_RIDDLES_CAMEO_DONOTUSE_51.wem
|
||||
Speech_RIDDLES_CAMEO_DONOTUSE_52.wem
|
||||
Speech_RIDDLES_CAMEO_DONOTUSE_53.wem
|
||||
Speech_RIDDLES_CAMEO_DONOTUSE_54.wem
|
||||
Speech_RIDDLES_CAMEO_DONOTUSE_55.wem
|
||||
Speech_RIDDLES_CAMEO_DONOTUSE_56.wem
|
||||
Speech_RIDDLES_CAMEO_DONOTUSE_57.wem
|
||||
Speech_RIDDLES_CAMEO_DONOTUSE_58.wem
|
||||
Speech_RIDDLES_CAMEO_DONOTUSE_59.wem
|
||||
Speech_RIDDLES_CAMEO_DONOTUSE_60.wem
|
||||
Speech_RIDDLES_CAMEO_DONOTUSE_61.wem
|
||||
Speech_RIDDLES_CAMEO_DONOTUSE_62.wem
|
||||
Speech_SEQUENCE_RIDDLER_CAPTURE_1.wem
|
||||
Speech_SEQUENCE_RIDDLER_CAPTURE_5.wem
|
||||
Speech_SEQUENCE_RIDDLER_CAPTURE_6.wem
|
||||
Speech_SEQUENCE_RIDDLER_CAPTURE_7.wem
|
||||
10
data/catalog/source/ita/Robin.txt
Normal file
10
data/catalog/source/ita/Robin.txt
Normal file
@@ -0,0 +1,10 @@
|
||||
Speech_SEQUENCE_CH4_MEET_ROBIN_3.wem
|
||||
Speech_SEQUENCE_CH4_MEET_ROBIN_4.wem
|
||||
Speech_SEQUENCE_CH4_MEET_ROBIN_8.wem
|
||||
Speech_SEQUENCE_CH4_MEET_ROBIN_9.wem
|
||||
Speech_SEQUENCE_CH4_MEET_ROBIN_12.wem
|
||||
Speech_SEQUENCE_CH4_MEET_ROBIN_16.wem
|
||||
Speech_SEQUENCE_CH4_MEET_ROBIN_17.wem
|
||||
Speech_SEQUENCE_CH4_MEET_ROBIN_20.wem
|
||||
Speech_UNDER_A1_CH5_BM_NW_3.wem
|
||||
Speech_UNDER_A1_CH5_BM_NW_8.wem
|
||||
27
data/catalog/source/ita/Stats.txt
Normal file
27
data/catalog/source/ita/Stats.txt
Normal file
@@ -0,0 +1,27 @@
|
||||
Alfred : 46
|
||||
Azrael : 10
|
||||
Bane : 45
|
||||
Batman : 789
|
||||
Calendar : 119
|
||||
Catwoman : 223
|
||||
Croc : 3
|
||||
Deadshot : 34
|
||||
Freeze : 238
|
||||
Gordon : 2
|
||||
Ivy : 54
|
||||
Joker : 289
|
||||
Madhatter: 40
|
||||
Oracle : 83
|
||||
Penguin : 296
|
||||
Quinn : 121
|
||||
Ras : 100
|
||||
Riddler : 353
|
||||
Robin : 10
|
||||
Strange : 182
|
||||
Talia : 50
|
||||
Twoface : 112
|
||||
Unknown : 33
|
||||
Vicky : 51
|
||||
Zsazz : 90
|
||||
|
||||
tot. 3373
|
||||
182
data/catalog/source/ita/Strange.txt
Normal file
182
data/catalog/source/ita/Strange.txt
Normal file
@@ -0,0 +1,182 @@
|
||||
CHR_RadioScanner_2.wem
|
||||
CHR_RadioScanner_3.wem
|
||||
CHR_RadioScanner_71.wem
|
||||
CHR_RadioScanner_79.wem
|
||||
Speech_CH_ZERO_CH0_close_door_1.wem
|
||||
Speech_CH_ZERO_CH0_ENCRYPTION_CARD2_2.wem
|
||||
Speech_CH_ZERO_CH0_strange_guardstoRoom_1.wem
|
||||
Speech_CH_ZERO_CH0_strange_picks_victim_3.wem
|
||||
Speech_CH_ZERO_CH0_strange_picks_victim_6.wem
|
||||
Speech_CH_ZERO_CH0_thug1_heckle_2.wem
|
||||
Speech_CHAR_STRANGE_CH0_Curfew_1.wem
|
||||
Speech_CHAR_STRANGE_CH0_Curfew_2.wem
|
||||
Speech_CHAR_STRANGE_CH0_Curfew_3.wem
|
||||
Speech_CHAR_STRANGE_CH0_Curfew_4.wem
|
||||
Speech_CHAR_STRANGE_Ch5_Strange_to_ARK_1.wem
|
||||
Speech_CHAR_STRANGE_Ch5_Strange_to_ARK_2.wem
|
||||
Speech_CHAR_STRANGE_CH6_P10_1_1.wem
|
||||
Speech_CHAR_STRANGE_CH6_P10_1_2.wem
|
||||
Speech_CHAR_STRANGE_CH6_P10_2_1.wem
|
||||
Speech_CHAR_STRANGE_CH6_P10_2_2.wem
|
||||
Speech_CHAR_STRANGE_CH7_Spotted_1.wem
|
||||
Speech_CHAR_STRANGE_CH7_Spotted_2.wem
|
||||
Speech_CHAR_STRANGE_PA_looping_1.wem
|
||||
Speech_CHAR_STRANGE_PA_looping_2.wem
|
||||
Speech_CHAR_STRANGE_PA_looping_3.wem
|
||||
Speech_CHAR_STRANGE_PA_looping_4.wem
|
||||
Speech_CHAR_STRANGE_PA_looping_5.wem
|
||||
Speech_CHAR_STRANGE_PA_looping_6.wem
|
||||
Speech_CHAR_STRANGE_PA_looping_7.wem
|
||||
Speech_CHAR_STRANGE_PA_looping_8.wem
|
||||
Speech_CHAR_STRANGE_PA_looping_9.wem
|
||||
Speech_CHAR_STRANGE_PA_looping_10.wem
|
||||
Speech_CHAR_STRANGE_PA_looping_11.wem
|
||||
Speech_CHAR_STRANGE_PROTOCOL_count_1.wem
|
||||
Speech_CHAR_STRANGE_PROTOCOL_count_2.wem
|
||||
Speech_CHAR_STRANGE_PROTOCOL_count_3.wem
|
||||
Speech_CHAR_STRANGE_PROTOCOL_count_4.wem
|
||||
Speech_CHAR_STRANGE_PROTOCOL_count_5.wem
|
||||
Speech_CHAR_STRANGE_PROTOCOL_count_6.wem
|
||||
Speech_CHAR_STRANGE_PROTOCOL_count_7.wem
|
||||
Speech_CHAR_STRANGE_PROTOCOL_count_8.wem
|
||||
Speech_CHAR_STRANGE_PROTOCOL_count_9.wem
|
||||
Speech_CHAR_STRANGE_PROTOCOL_count_10.wem
|
||||
Speech_CHAR_STRANGE_PROTOCOL_count_11.wem
|
||||
Speech_CHAR_STRANGE_PROTOCOL_count_12.wem
|
||||
Speech_CHAR_STRANGE_PROTOCOL_count_13.wem
|
||||
Speech_CHAR_STRANGE_PROTOCOL_count_14.wem
|
||||
Speech_CHAR_STRANGE_PROTOCOL_count_15.wem
|
||||
Speech_CHAR_STRANGE_PROTOCOL_count_16.wem
|
||||
Speech_CHAR_STRANGE_PROTOCOL_count_17.wem
|
||||
Speech_CHAR_STRANGE_PROTOCOL_count_18.wem
|
||||
Speech_CHAR_STRANGE_PROTOCOL_count_19.wem
|
||||
Speech_CINEMATIC_SPECIAL_NEW_INTRO_5.wem
|
||||
Speech_CINEMATIC_SPECIAL_NEW_INTRO_12.wem
|
||||
Speech_CINEMATIC_SPECIAL_NEW_INTRO_19.wem
|
||||
Speech_CINEMATIC_SPECIAL_NEW_INTRO_21.wem
|
||||
Speech_GAME_OVER_Hugo_Strange_1.wem
|
||||
Speech_GAME_OVER_Hugo_Strange_2.wem
|
||||
Speech_GAME_OVER_Hugo_Strange_3.wem
|
||||
Speech_GAME_OVER_Hugo_Strange_4.wem
|
||||
Speech_GAME_OVER_Hugo_Strange_5.wem
|
||||
Speech_GAME_OVER_Hugo_Strange_6.wem
|
||||
Speech_GAME_OVER_Hugo_Strange_7.wem
|
||||
Speech_GAME_OVER_Hugo_Strange_8.wem
|
||||
Speech_MAP_CH2_TYGER_copter_spots_flare_2.wem
|
||||
Speech_MAP_CH3_Lost_contact_church_2.wem
|
||||
Speech_MAP_CH3_Lost_contact_church_4.wem
|
||||
Speech_MAP_CH3_Tyger_Discovered_Hush_Victim_1_1.wem
|
||||
Speech_PRED_MAYOR_AUTOMATED_1.wem
|
||||
Speech_PRED_MAYOR_AUTOMATED_2.wem
|
||||
Speech_PRED_MAYOR_AUTOMATED_3.wem
|
||||
Speech_PRED_MAYOR_AUTOMATED_4.wem
|
||||
Speech_PRED_MAYOR_AUTOMATED_5.wem
|
||||
Speech_PRED_MAYOR_AUTOMATED_6.wem
|
||||
Speech_PRED_MAYOR_AUTOMATED_7.wem
|
||||
Speech_PRED_MAYOR_AUTOMATED_8.wem
|
||||
Speech_PRED_MAYOR_AUTOMATED_9.wem
|
||||
Speech_PRED_MAYOR_AUTOMATED_10.wem
|
||||
Speech_PRED_MAYOR_AUTOMATED_11.wem
|
||||
Speech_PRED_MAYOR_AUTOMATED_12.wem
|
||||
Speech_PRED_MAYOR_AUTOMATED_13.wem
|
||||
Speech_PRED_MAYOR_AUTOMATED_14.wem
|
||||
Speech_PRED_MAYOR_AUTOMATED_15.wem
|
||||
Speech_PRED_MAYOR_AUTOMATED_16.wem
|
||||
Speech_PRED_MAYOR_AUTOMATED_17.wem
|
||||
Speech_PRED_MAYOR_AUTOMATED_18.wem
|
||||
Speech_PRED_MAYOR_AUTOMATED_19.wem
|
||||
Speech_PRED_MAYOR_AUTOMATED_20.wem
|
||||
Speech_PRED_MAYOR_AUTOMATED_21.wem
|
||||
Speech_PRED_MAYOR_AUTOMATED_22.wem
|
||||
Speech_PRED_MAYOR_AUTOMATED_23.wem
|
||||
Speech_PRED_MAYOR_AUTOMATED_24.wem
|
||||
Speech_PRED_MAYOR_AUTOMATED_25.wem
|
||||
Speech_PRED_MAYOR_AUTOMATED_26.wem
|
||||
Speech_PRED_MAYOR_AUTOMATED_27.wem
|
||||
Speech_PRED_MAYOR_AUTOMATED_28.wem
|
||||
Speech_PRED_MAYOR_AUTOMATED_29.wem
|
||||
Speech_PRED_MAYOR_AUTOMATED_30.wem
|
||||
Speech_PRED_MAYOR_AUTOMATED_31.wem
|
||||
Speech_PRED_MAYOR_AUTOMATED_32.wem
|
||||
Speech_PRED_MAYOR_AUTOMATED_33.wem
|
||||
Speech_PRED_MAYOR_AUTOMATED_COMMS_back_first_1.wem
|
||||
Speech_PRED_MAYOR_AUTOMATED_NumDownOne_1.wem
|
||||
Speech_PRED_MAYOR_AUTOMATED_NumDownThree_1.wem
|
||||
Speech_PRED_MAYOR_CH7_ending_1.wem
|
||||
Speech_PRED_MAYOR_CH7_ending_2.wem
|
||||
Speech_PRED_STRANGE_Batman_in_room_location_unknown_1.wem
|
||||
Speech_PRED_STRANGE_Batman_in_room_location_unknown_2.wem
|
||||
Speech_PRED_STRANGE_Batman_in_room_location_unknown_3.wem
|
||||
Speech_PRED_STRANGE_Batman_in_room_location_unknown_4.wem
|
||||
Speech_PRED_STRANGE_Batman_in_room_location_unknown_5.wem
|
||||
Speech_PRED_STRANGE_Batman_in_room_location_unknown_6.wem
|
||||
Speech_PRED_STRANGE_Batman_in_room_location_unknown_7.wem
|
||||
Speech_PRED_STRANGE_Bm_tries_door_Tyger_active_1.wem
|
||||
Speech_PRED_STRANGE_Bm_tries_door_Tyger_active_2.wem
|
||||
Speech_PRED_STRANGE_Bm_tries_door_Tyger_active_3.wem
|
||||
Speech_PRED_STRANGE_IDLES_1.wem
|
||||
Speech_PRED_STRANGE_IDLES_2.wem
|
||||
Speech_PRED_STRANGE_IDLES_3.wem
|
||||
Speech_PRED_STRANGE_IDLES_4.wem
|
||||
Speech_PRED_STRANGE_IDLES_5.wem
|
||||
Speech_PRED_STRANGE_Men_taken_out_BM_unknown_1.wem
|
||||
Speech_PRED_STRANGE_Men_taken_out_BM_unknown_2.wem
|
||||
Speech_PRED_STRANGE_Men_taken_out_BM_unknown_3.wem
|
||||
Speech_PRED_STRANGE_Men_taken_out_BM_unknown_4.wem
|
||||
Speech_PRED_STRANGE_Men_taken_out_BM_unknown_5.wem
|
||||
Speech_PRED_STRANGE_Men_taken_out_BM_unknown_6.wem
|
||||
Speech_PRED_STRANGE_Men_taken_out_BM_unknown_7.wem
|
||||
Speech_PRED_STRANGE_See_Batman_1.wem
|
||||
Speech_PRED_STRANGE_See_Batman_2.wem
|
||||
Speech_PRED_STRANGE_See_Batman_3.wem
|
||||
Speech_PRED_STRANGE_See_Batman_4.wem
|
||||
Speech_PRED_STRANGE_See_Batman_5.wem
|
||||
Speech_PRED_STRANGE_Strange_to_Tyger_1.wem
|
||||
Speech_PRED_STRANGE_Strange_to_Tyger_2.wem
|
||||
Speech_PRED_STRANGE_Strange_to_Tyger_3.wem
|
||||
Speech_PRED_STRANGE_Strange_to_Tyger_4.wem
|
||||
Speech_PRED_STRANGE_Strange_to_Tyger_5.wem
|
||||
Speech_PRED_STRANGE_Strange_to_Tyger_6.wem
|
||||
Speech_PRED_STRANGE_Strange_to_Tyger_7.wem
|
||||
Speech_PRED_STRANGE_Strange_to_Tyger_8.wem
|
||||
Speech_PRED_STRANGE_Taunts_all_taken_down_1.wem
|
||||
Speech_PRED_STRANGE_Taunts_all_taken_down_2.wem
|
||||
Speech_PRED_STRANGE_Taunts_all_taken_down_3.wem
|
||||
Speech_PRED_STRANGE_Taunts_all_taken_down_4.wem
|
||||
Speech_PRED_STRANGE_Taunts_all_taken_down_5.wem
|
||||
Speech_RADIO_SCAN_CH3_road_explosion_3.wem
|
||||
Speech_RADIO_SCAN_CH7_Attack_museum2_1.wem
|
||||
Speech_RADIO_SCAN_CH7_End_search_2.wem
|
||||
Speech_RADIO_SCAN_CH7_Fire_at_museum_1.wem
|
||||
Speech_RADIO_SCAN_CH7_Fire_at_museum_4.wem
|
||||
Speech_RADIO_SCAN_CH7_search_tower_3.wem
|
||||
Speech_RADIO_SCAN_CH7_under_speech_1.wem
|
||||
Speech_RADIO_SCAN_CH7_under_speech_2.wem
|
||||
Speech_RADIO_SCAN_CH7_under_speech_3.wem
|
||||
Speech_RADIO_SCAN_FIRST_copter_spot_BM_1.wem
|
||||
Speech_SEQUENCE_CH7_UNDER_A3_INTRO_1.wem
|
||||
Speech_SIDE_CAT_Batdown_2.wem
|
||||
Speech_SIDE_CAT_Batman_Joker_spotted_4.wem
|
||||
Speech_SIDE_CAT_Batman_Joker_spotted_8.wem
|
||||
Speech_SIDE_CAT_Ch6_vault_intro_1.wem
|
||||
Speech_SIDE_CAT_Ch6_vault_intro_2.wem
|
||||
Speech_SIDE_CAT_Strange_lockdown_1.wem
|
||||
Speech_SIDE_CAT_Strange_lockdown_2.wem
|
||||
Speech_STEEL_C5_OR_INT_COUNCIL_2_5.wem
|
||||
Speech_STREETS_A5_CH7_processing_intro_2.wem
|
||||
Speech_STREETS_A5_CH7_processing_intro_5.wem
|
||||
Speech_TV_CH7_Under_C4_fightover_1.wem
|
||||
Speech_TV_CH7_Under_C4_fightover_2.wem
|
||||
Speech_TV_CH7_UNDER_C4_STRANGE_1.wem
|
||||
Speech_TV_CH7_UNDER_C4_STRANGE_2.wem
|
||||
Speech_TV_CH7_UNDER_C4_STRANGE_3.wem
|
||||
Speech_TV_CH7_UNDER_C4_STRANGE_4.wem
|
||||
Speech_TV_CH7_UNDER_C4_STRANGE_5.wem
|
||||
Speech_TV_CH7_UNDER_C4_STRANGE_6.wem
|
||||
Speech_TV_CH7_UNDER_C4_STRANGE_7.wem
|
||||
Speech_TV_CH7_UNDER_C4_STRANGE_8.wem
|
||||
Speech_TV_CH7_UNDER_C4_STRANGE_9.wem
|
||||
Speech_TV_CH7_UNDER_C4_STRANGE_10.wem
|
||||
Speech_UNDER_A1_CH7_elev_STRANGE_1.wem
|
||||
Speech_UNDER_A1_CH7_elev_STRANGE_2.wem
|
||||
Speech_UNDER_A1_CH7_elev_STRANGE_3.wem
|
||||
50
data/catalog/source/ita/Talia.txt
Normal file
50
data/catalog/source/ita/Talia.txt
Normal file
@@ -0,0 +1,50 @@
|
||||
Speech_CINEMATIC_CH4_MEET_TALIA_3.wem
|
||||
Speech_CINEMATIC_CH4_MEET_TALIA_4.wem
|
||||
Speech_CINEMATIC_CH4_MEET_TALIA_5.wem
|
||||
Speech_CINEMATIC_CH4_MEET_TALIA_7.wem
|
||||
Speech_CINEMATIC_CH4_MEET_TALIA_9.wem
|
||||
Speech_CINEMATIC_CH4_MEET_TALIA_10.wem
|
||||
Speech_CINEMATIC_CH4_MEET_TALIA_11.wem
|
||||
Speech_CINEMATIC_CH4_MEET_TALIA_12.wem
|
||||
Speech_CINEMATIC_CH4_MEET_TALIA_15.wem
|
||||
Speech_CINEMATIC_CH4_MEET_TALIA_16.wem
|
||||
Speech_CINEMATIC_CH4_MEET_TALIA_18.wem
|
||||
Speech_CINEMATIC_CH4_MEET_TALIA_19.wem
|
||||
Speech_CINEMATIC_CH4_MEET_TALIA_20.wem
|
||||
Speech_CINEMATIC_CH4_MEET_TALIA_21.wem
|
||||
Speech_CINEMATIC_CH4_MEET_TALIA_22.wem
|
||||
Speech_CINEMATIC_CH4_MEET_TALIA_24.wem
|
||||
Speech_CINEMATIC_CH8_TALIA_KILLS_THE_WRONG_JOKER_4.wem
|
||||
Speech_CINEMATIC_CH8_TALIA_KILLS_THE_WRONG_JOKER_7.wem
|
||||
Speech_CINEMATIC_CH8_TALIA_KILLS_THE_WRONG_JOKER_26.wem
|
||||
Speech_CINEMATIC_CH8_TALIA_KILLS_THE_WRONG_JOKER_27.wem
|
||||
Speech_CINEMATIC_CH8_TALIA_KILLS_THE_WRONG_JOKER_30.wem
|
||||
Speech_CINEMATIC_CH8_TALIA_KILLS_THE_WRONG_JOKER_33.wem
|
||||
Speech_CINEMATIC_CH8_TALIA_KILLS_THE_WRONG_JOKER_34.wem
|
||||
Speech_GAME_OVER_Talia_al_Ghul_1.wem
|
||||
Speech_GAME_OVER_Talia_al_Ghul_2.wem
|
||||
Speech_GAME_OVER_Talia_al_Ghul_3.wem
|
||||
Speech_SEQUENCE_CH4_UNDER_B1_TALIA_DRINK_1.wem
|
||||
Speech_SEQUENCE_CH4_UNDER_B1_TALIA_DRINK_2.wem
|
||||
Speech_SEQUENCE_CH4_UNDER_B1_TALIA_DRINK_3.wem
|
||||
Speech_SEQUENCE_CH4_UNDER_B1_TALIA_DRINK_5.wem
|
||||
Speech_SEQUENCE_CH4_UNDER_B1_TALIA_DRINK_7.wem
|
||||
Speech_SEQUENCE_CH4_UNDER_B1_TALIA_DRINK_9.wem
|
||||
Speech_UNDER_B1_CH4_TALIA_NONE_1.wem
|
||||
Speech_UNDER_B1_CH4_TALIA_NONE_2.wem
|
||||
Speech_UNDER_B1_CH4_TALIA_WALK_1.wem
|
||||
Speech_UNDER_B1_CH4_TALIA_WALK_2.wem
|
||||
Speech_UNDER_B1_CH4_TALIA_WALK_4.wem
|
||||
Speech_UNDER_B1_CH4_TALIA_WALK_5.wem
|
||||
Speech_UNDER_S1_Boss_conv1B_2.wem
|
||||
Speech_UNDER_S1_Boss_conv1B_3.wem
|
||||
Speech_UNDER_S1_Boss_conv1B_4.wem
|
||||
Speech_UNDER_S1_Boss_conv2_2.wem
|
||||
Speech_UNDER_S1_Boss_conv2_3.wem
|
||||
Speech_UNDER_S1_Talia_idles_1.wem
|
||||
Speech_UNDER_S1_Talia_idles_2.wem
|
||||
Speech_UNDER_S1_Talia_idles_3.wem
|
||||
Speech_UNDER_S1_Talia_idles_4.wem
|
||||
Speech_UNDER_S1_Talia_TrialsDone_3.wem
|
||||
Speech_UNDER_S1_Talia_TrialsDone_4.wem
|
||||
Speech_UNDER_S1_Talia_TrialsDone_5.wem
|
||||
112
data/catalog/source/ita/Twoface.txt
Normal file
112
data/catalog/source/ita/Twoface.txt
Normal file
@@ -0,0 +1,112 @@
|
||||
Speech_CINEMATIC_CH1_CATWOMAN_SAVED_4.wem
|
||||
Speech_CINEMATIC_CH1_CATWOMAN_SAVED_7.wem
|
||||
Speech_CINEMATIC_CH1_CATWOMAN_SAVED_10.wem
|
||||
Speech_CINEMATIC_CH1_CATWOMAN_SAVED_23.wem
|
||||
Speech_CINEMATIC_CH1_CATWOMAN_SAVED_30.wem
|
||||
Speech_COURT_B1_CH1_TwoFace_1_2.wem
|
||||
Speech_COURT_B1_CH1_TwoFace_1_3.wem
|
||||
Speech_COURT_B1_CH1_TwoFace_1_4.wem
|
||||
Speech_COURT_B1_CH1_TwoFace_1_4.wem
|
||||
Speech_COURT_B1_CH1_TwoFace_2_1.wem
|
||||
Speech_COURT_B1_CH1_TwoFace_2_2.wem
|
||||
Speech_COURT_B1_CH1_TwoFace_2_3.wem
|
||||
Speech_COURT_B1_CH1_TwoFace_Court_1.wem
|
||||
Speech_COURT_B1_CH1_TwoFace_Court_2.wem
|
||||
Speech_COURT_B1_CH1_TwoFace_Court_3.wem
|
||||
Speech_COURT_B1_CH1_TwoFace_Court_4.wem
|
||||
Speech_COURT_B1_CH1_TwoFace_Court_5.wem
|
||||
Speech_COURT_B1_CH1_TwoFace_Court_6.wem
|
||||
Speech_COURT_B1_CH1_TwoFace_Court_7.wem
|
||||
Speech_COURT_B1_CH1_TwoFace_Court_8.wem
|
||||
Speech_COURT_B1_CH1_TwoFace_Court_9.wem
|
||||
Speech_COURT_B1_CH1_TwoFace_Court_10.wem
|
||||
Speech_COURT_B1_CH1_TwoFace_Court_11.wem
|
||||
Speech_COURT_B1_CH1_TwoFace_Court_12.wem
|
||||
Speech_COURT_B1_CH1_TwoFace_Court_13.wem
|
||||
Speech_COURT_B1_CH1_TwoFace_Court_14.wem
|
||||
Speech_COURT_B1_CH1_TwoFace_Court_15.wem
|
||||
Speech_COURT_B1_CH1_TwoFace_Court_16.wem
|
||||
Speech_COURT_B1_CH1_TwoFace_Court_17.wem
|
||||
Speech_COURT_B1_CH1_TwoFace_Court_19.wem
|
||||
Speech_COURT_B1_CH1_TwoFace_Court_21.wem
|
||||
Speech_COURT_B1_CH1_TwoFace_Court_22.wem
|
||||
Speech_COURT_B1_CH1_TwoFace_Court_24.wem
|
||||
Speech_COURT_B1_CH1_TwoFace_Kill_her_1.wem
|
||||
Speech_COURT_B1_CH1_TwoFace_Kill_her_2.wem
|
||||
Speech_COURT_B1_CH1_TwoFace_Kill_her_3.wem
|
||||
Speech_COURT_B1_Ch1_TwoFace_Order_1.wem
|
||||
Speech_COURT_B1_Ch1_TwoFace_Order_2.wem
|
||||
Speech_COURT_B1_Ch1_TwoFace_Order_3.wem
|
||||
Speech_COURT_B1_Ch1_TwoFace_Order_4.wem
|
||||
Speech_COURT_B1_Ch1_TwoFace_Order_5.wem
|
||||
Speech_COURT_B1_Ch1_TwoFace_Order_6.wem
|
||||
Speech_COURT_B1_Ch1_TwoFace_Order_7.wem
|
||||
Speech_COURT_B1_Ch1_TwoFace_Order_8.wem
|
||||
Speech_COURT_B1_Ch1_TwoFace_Order_9.wem
|
||||
Speech_COURT_B1_Ch1_TwoFace_Order_10.wem
|
||||
Speech_COURT_B1_Ch1_TwoFace_Order_11.wem
|
||||
Speech_COURT_B1_Ch1_TwoFace_Order_12.wem
|
||||
Speech_COURT_B1_CH1_TwoFace_Rand_1.wem
|
||||
Speech_COURT_B1_CH1_TwoFace_Rand_2.wem
|
||||
Speech_COURT_B1_CH1_TwoFace_Rand_3.wem
|
||||
Speech_COURT_B1_CH1_TwoFace_Rand_4.wem
|
||||
Speech_COURT_B1_CH1_TwoFace_Rand_5.wem
|
||||
Speech_COURT_B1_Ch2_Batman_spotted_1.wem
|
||||
Speech_COURT_B1_CH2_TF_Hang_conv_1.wem
|
||||
Speech_COURT_B1_CH2_TF_Hang_conv_2.wem
|
||||
Speech_COURT_B1_CH2_TF_Hang_conv_3.wem
|
||||
Speech_COURT_B1_CH2_TF_HITBM_1.wem
|
||||
Speech_COURT_B1_CH2_TF_HITBM_2.wem
|
||||
Speech_COURT_B1_CH2_TF_HITBM_3.wem
|
||||
Speech_COURT_B1_CH2_TF_HITTHUG_1.wem
|
||||
Speech_COURT_B1_CH2_TF_HITTHUG_2.wem
|
||||
Speech_COURT_B1_CH2_TF_HITTHUG_3.wem
|
||||
Speech_COURT_B1_CH2_TF_Idles_1.wem
|
||||
Speech_COURT_B1_CH2_TF_Idles_2.wem
|
||||
Speech_COURT_B1_CH2_TF_Idles_3.wem
|
||||
Speech_COURT_B1_CH2_TF_MissBM_1.wem
|
||||
Speech_COURT_B1_CH2_TF_MissBM_2.wem
|
||||
Speech_COURT_B1_CH2_TF_Shoot_1.wem
|
||||
Speech_COURT_B1_CH2_TF_Shoot_2.wem
|
||||
Speech_COURT_B1_CH2_TF_Shoot_3.wem
|
||||
Speech_COURT_B1_CH2_TF_Shoot_4.wem
|
||||
Speech_GAME_OVER_Two_Face_1.wem
|
||||
Speech_GAME_OVER_Two_Face_2.wem
|
||||
Speech_GAME_OVER_Two_Face_3.wem
|
||||
Speech_GAME_OVER_Two_Face_4.wem
|
||||
Speech_GAME_OVER_Two_Face_5.wem
|
||||
Speech_GAME_OVER_Two_Face_6.wem
|
||||
Speech_GAME_OVER_Two_Face_7.wem
|
||||
Speech_GAME_OVER_Two_Face_8.wem
|
||||
Speech_GAME_OVER_Two_Face_9.wem
|
||||
Speech_SEQUENCE_TWOFACE_END2_3.wem
|
||||
Speech_SEQUENCE_TWOFACE_END2_4.wem
|
||||
Speech_SEQUENCE_TWOFACE_END2_8.wem
|
||||
Speech_SEQUENCE_TWOFACE_END_2.wem
|
||||
Speech_SEQUENCE_TWOFACE_END_6.wem
|
||||
Speech_SIDE_CAT_TF_Backup_1.wem
|
||||
Speech_SIDE_CAT_TF_Backup_2.wem
|
||||
Speech_SIDE_CAT_TF_Backup_3.wem
|
||||
Speech_SIDE_CAT_TF_fire_1.wem
|
||||
Speech_SIDE_CAT_TF_fire_2.wem
|
||||
Speech_SIDE_CAT_TF_fire_3.wem
|
||||
Speech_SIDE_CAT_TF_fire_4.wem
|
||||
Speech_SIDE_CAT_TF_fire_5.wem
|
||||
Speech_SIDE_CAT_TF_fire_6.wem
|
||||
Speech_SIDE_CAT_TF_intro1_1.wem
|
||||
Speech_SIDE_CAT_TF_intro1_2.wem
|
||||
Speech_SIDE_CAT_TF_intro1_3.wem
|
||||
Speech_SIDE_CAT_TF_intro1_4.wem
|
||||
Speech_SIDE_CAT_TF_intro1_5.wem
|
||||
Speech_SIDE_CAT_TF_intro1_6.wem
|
||||
Speech_SIDE_CAT_TF_intro1_7.wem
|
||||
Speech_SIDE_CAT_TF_look_for_her_1.wem
|
||||
Speech_SIDE_CAT_TF_look_for_her_2.wem
|
||||
Speech_SIDE_CAT_TF_look_for_her_3.wem
|
||||
Speech_SIDE_CAT_TF_look_for_her_4.wem
|
||||
Speech_SIDE_CAT_TF_look_for_her_5.wem
|
||||
Speech_SIDE_CAT_TF_shes_here_1.wem
|
||||
Speech_SIDE_CAT_TF_shes_here_2.wem
|
||||
Speech_SIDE_CAT_TF_shes_here_3.wem
|
||||
Speech_SIDE_CAT_TF_shes_here_4.wem
|
||||
Speech_SIDE_CAT_TF_shes_here_5.wem
|
||||
33
data/catalog/source/ita/Unknown.txt
Normal file
33
data/catalog/source/ita/Unknown.txt
Normal file
@@ -0,0 +1,33 @@
|
||||
Speech_CH_ZERO_CH0_thug1_heckle_1.wem
|
||||
Speech_CH_ZERO_CH0_thug1_heckle_3.wem
|
||||
Speech_CH_ZERO_CH0_thug_ryder_1.wem
|
||||
Speech_CH_ZERO_CH0_thug_ryder_2.wem
|
||||
Speech_CH_ZERO_CH0_thug_ryder_3.wem
|
||||
Speech_CH_ZERO_CH0_thug_ryder_4.wem
|
||||
Speech_CH_ZERO_CH0_thug_ryder_5.wem
|
||||
Speech_CINEMATIC_CH1_CATWOMAN_SAVED_21.wem
|
||||
Speech_CINEMATIC_SPECIAL_NEW_INTRO_1.wem
|
||||
Speech_CINEMATIC_SPECIAL_NEW_INTRO_3.wem
|
||||
Speech_CINEMATIC_SPECIAL_NEW_INTRO_8.wem
|
||||
Speech_CINEMATIC_SPECIAL_NEW_INTRO_9.wem
|
||||
Speech_CINEMATIC_SPECIAL_NEW_INTRO_10.wem
|
||||
Speech_CINEMATIC_SPECIAL_NEW_INTRO_11.wem
|
||||
Speech_CINEMATIC_SPECIAL_NEW_INTRO_20.wem
|
||||
Speech_COURT_B1_CH1_TwoFace_1_3.wem
|
||||
Speech_COURT_B1_CH1_TwoFace_1_4.wem
|
||||
Speech_COURT_B1_CH1_TwoFace_2_1.wem
|
||||
Speech_COURT_B1_CH1_TwoFace_2_2.wem
|
||||
Speech_COURT_B1_CH1_TwoFace_2_3.wem
|
||||
Speech_COURT_B1_CH1_TwoFace_Court_18.wem
|
||||
Speech_INTERROGATION_Riddler_06_2.wem
|
||||
Speech_SEQUENCE_CH4_MEET_ROBIN_5.wem
|
||||
Speech_SEQUENCE_TWOFACE_END2_9.wem
|
||||
Speech_SIDE_BANE_Post_intro_idles_1.wem
|
||||
Speech_SIDE_BANE_Post_intro_idles_2.wem
|
||||
Speech_SIDE_BANE_Post_intro_idles_3.wem
|
||||
Speech_UNDER_C4_CH7_Batman_lose_1.wem
|
||||
Speech_UNDER_C4_CH7_Batman_lose_2.wem
|
||||
Speech_UNDER_C4_CH7_Batman_lose_3.wem
|
||||
Speech_UNDER_C4_CH7_Batman_Wins_1.wem
|
||||
Speech_UNDER_C4_CH7_Batman_Wins_2.wem
|
||||
Speech_UNDER_C4_CH7_Batman_Wins_3.wem
|
||||
51
data/catalog/source/ita/Vicky.txt
Normal file
51
data/catalog/source/ita/Vicky.txt
Normal file
@@ -0,0 +1,51 @@
|
||||
CHR_RadioScanner_4.wem
|
||||
CHR_RadioScanner_13.wem
|
||||
CHR_RadioScanner_17.wem
|
||||
CHR_RadioScanner_20.wem
|
||||
CHR_RadioScanner_27.wem
|
||||
CHR_RadioScanner_37.wem
|
||||
CHR_RadioScanner_39.wem
|
||||
CHR_RadioScanner_51.wem
|
||||
CHR_RadioScanner_58.wem
|
||||
CHR_RadioScanner_60.wem
|
||||
CHR_RadioScanner_65.wem
|
||||
CHR_RadioScanner_76.wem
|
||||
Speech_CHURCH_B1_Riddler_saves_Sharp_Vale_conv_3.wem
|
||||
Speech_CHURCH_B1_Riddler_saves_Sharp_Vale_conv_5.wem
|
||||
Speech_CHURCH_B1_Riddler_saves_Sharp_Vale_conv_6.wem
|
||||
Speech_CINEMATIC_SPECIAL_NEW_INTRO_7.wem
|
||||
Speech_CINEMATIC_SPECIAL_NEW_INTRO_15.wem
|
||||
Speech_RADIO_SCAN_CH1_VV_1.wem
|
||||
Speech_RADIO_SCAN_CH1_VV_2.wem
|
||||
Speech_RADIO_SCAN_CH1_VV_3.wem
|
||||
Speech_RADIO_SCAN_CH1_VV_4.wem
|
||||
Speech_RADIO_SCAN_CH6_VV_BM_Comment_1.wem
|
||||
Speech_RADIO_SCAN_CH6_VV_BM_Comment_2.wem
|
||||
Speech_RADIO_SCAN_CH6_VV_BM_Comment_3.wem
|
||||
Speech_RADIO_SCAN_CH6_VV_BM_Comment_4.wem
|
||||
Speech_RADIO_SCAN_CH6_VV_BM_Losing_1.wem
|
||||
Speech_RADIO_SCAN_CH6_VV_BM_Losing_2.wem
|
||||
Speech_RADIO_SCAN_CH6_VV_BM_Losing_3.wem
|
||||
Speech_RADIO_SCAN_CH6_VV_BM_Winning_1.wem
|
||||
Speech_RADIO_SCAN_CH6_VV_BM_Winning_2.wem
|
||||
Speech_RADIO_SCAN_CH6_VV_BM_Winning_3.wem
|
||||
Speech_RADIO_SCAN_CH6_VV_SniperIdles_1.wem
|
||||
Speech_RADIO_SCAN_CH6_VV_SniperIdles_2.wem
|
||||
Speech_RADIO_SCAN_CH6_VV_SniperIdles_3.wem
|
||||
Speech_RADIO_SCAN_CH6_VV_SniperIdles_4.wem
|
||||
Speech_SIDE_STORY_VV_post_rescued_big_speech_1.wem
|
||||
Speech_SIDE_STORY_VV_post_rescued_big_speech_2.wem
|
||||
Speech_SIDE_STORY_VV_post_rescued_big_speech_3.wem
|
||||
Speech_SIDE_STORY_VV_post_rescued_big_speech_4.wem
|
||||
Speech_SIDE_STORY_VV_post_rescued_big_speech_5.wem
|
||||
Speech_SIDE_STORY_VV_post_rescued_big_speech_6.wem
|
||||
Speech_SIDE_STORY_VV_post_rescued_big_speech_7.wem
|
||||
Speech_SIDE_STORY_VV_pre_rescue_snipers_2.wem
|
||||
Speech_SIDE_STORY_VV_saved_conv_5.wem
|
||||
Speech_SIDE_STORY_VV_saved_conv_6.wem
|
||||
Speech_SIDE_STORY_VV_Saved_idles_1.wem
|
||||
Speech_SIDE_STORY_VV_Saved_idles_2.wem
|
||||
Speech_SIDE_STORY_VV_Saved_idles_3.wem
|
||||
Speech_SIDE_STORY_VV_Saved_idles_4.wem
|
||||
Speech_SIDE_STORY_VV_Shoot_copter_1.wem
|
||||
Speech_SIDE_STORY_VV_Shoot_copter_2.wem
|
||||
90
data/catalog/source/ita/Zsazz.txt
Normal file
90
data/catalog/source/ita/Zsazz.txt
Normal file
@@ -0,0 +1,90 @@
|
||||
Speech_CHAR_ZSASZ_CALL1GLOAT_1.wem
|
||||
Speech_CHAR_ZSASZ_CALL1GLOAT_2.wem
|
||||
Speech_CHAR_ZSASZ_CALL2GLOAT_2.wem
|
||||
Speech_CHAR_ZSASZ_CALL2GLOAT_3.wem
|
||||
Speech_CHAR_ZSASZ_CALL2GLOAT_4.wem
|
||||
Speech_CHAR_ZSASZ_CALL2GLOAT_5.wem
|
||||
Speech_CHAR_ZSASZ_CALL3GLOAT_1.wem
|
||||
Speech_CHAR_ZSASZ_CALL3GLOAT_2.wem
|
||||
Speech_CHAR_ZSASZ_CALL3GLOAT_3.wem
|
||||
Speech_CHAR_ZSASZ_CALL3GLOAT_4.wem
|
||||
Speech_CHAR_ZSASZ_CALL4GLOAT_1.wem
|
||||
Speech_CHAR_ZSASZ_CALL4GLOAT_2.wem
|
||||
Speech_CHAR_ZSASZ_CALL4GLOAT_3.wem
|
||||
Speech_CHAR_ZSASZ_CALL4GLOAT_4.wem
|
||||
Speech_CHAR_ZSASZ_CALL5GLOAT_1.wem
|
||||
Speech_CHAR_ZSASZ_CALL5GLOAT_2.wem
|
||||
Speech_CHAR_ZSASZ_CALL5GLOAT_3.wem
|
||||
Speech_CHAR_ZSASZ_CALL6GLOAT_1.wem
|
||||
Speech_CHAR_ZSASZ_CALL6GLOAT_2.wem
|
||||
Speech_CHAR_ZSASZ_CALL6GLOAT_3.wem
|
||||
Speech_CHAR_ZSASZ_CALL6GLOAT_4.wem
|
||||
Speech_CHAR_ZSASZ_CALL7GLOAT_1.wem
|
||||
Speech_CHAR_ZSASZ_CALL7GLOAT_2.wem
|
||||
Speech_CHAR_ZSASZ_CALL7GLOAT_3.wem
|
||||
Speech_CHAR_ZSASZ_CALL8GLOAT_2.wem
|
||||
Speech_CHAR_ZSASZ_CALL8GLOAT_4.wem
|
||||
Speech_CHAR_ZSASZ_CALL8GLOAT_5.wem
|
||||
Speech_CHAR_ZSASZ_Find_me_1.wem
|
||||
Speech_CHAR_ZSASZ_Find_me_2.wem
|
||||
Speech_CHAR_ZSASZ_Find_me_3.wem
|
||||
Speech_CHAR_ZSASZ_Find_me_4.wem
|
||||
Speech_CHAR_ZSASZ_Find_me_5.wem
|
||||
Speech_CHAR_ZSASZ_Find_me_6.wem
|
||||
Speech_CHAR_ZSASZ_Find_me_7.wem
|
||||
Speech_CHAR_ZSASZ_Find_me_8.wem
|
||||
Speech_CHAR_ZSASZ_Find_me_9.wem
|
||||
Speech_CHAR_ZSASZ_Find_me_10.wem
|
||||
Speech_CHAR_ZSASZ_Find_me_first_4.wem
|
||||
Speech_CHAR_ZSASZ_Find_me_first_5.wem
|
||||
Speech_CHAR_ZSASZ_Find_me_first_6.wem
|
||||
Speech_CHAR_ZSASZ_Find_me_first_7.wem
|
||||
Speech_CHAR_ZSASZ_Find_me_first_8.wem
|
||||
Speech_CHAR_ZSASZ_Tease_1.wem
|
||||
Speech_CHAR_ZSASZ_Tease_3.wem
|
||||
Speech_GAME_OVER_Zsasz_1.wem
|
||||
Speech_GAME_OVER_Zsasz_2.wem
|
||||
Speech_GAME_OVER_Zsasz_3.wem
|
||||
Speech_GAME_OVER_Zsasz_4.wem
|
||||
Speech_GAME_OVER_Zsasz_5.wem
|
||||
Speech_SIDE_STORY_Zsasz_BM_fail_1.wem
|
||||
Speech_SIDE_STORY_Zsasz_BM_fail_2.wem
|
||||
Speech_SIDE_STORY_Zsasz_BM_fail_3.wem
|
||||
Speech_SIDE_STORY_Zsasz_BM_fail_4.wem
|
||||
Speech_SIDE_STORY_Zsasz_cage_idles_1.wem
|
||||
Speech_SIDE_STORY_Zsasz_cage_idles_2.wem
|
||||
Speech_SIDE_STORY_Zsasz_cage_idles_3.wem
|
||||
Speech_SIDE_STORY_Zsasz_call_BM_1.wem
|
||||
Speech_SIDE_STORY_Zsasz_call_BM_2.wem
|
||||
Speech_SIDE_STORY_Zsasz_call_BM_3.wem
|
||||
Speech_SIDE_STORY_Zsasz_call_BM_4.wem
|
||||
Speech_SIDE_STORY_Zsasz_call_BM_5.wem
|
||||
Speech_SIDE_STORY_Zsasz_call_BM_idles_1.wem
|
||||
Speech_SIDE_STORY_Zsasz_call_BM_idles_2.wem
|
||||
Speech_SIDE_STORY_Zsasz_call_BM_idles_3.wem
|
||||
Speech_SIDE_STORY_Zsasz_call_BM_idles_4.wem
|
||||
Speech_SIDE_STORY_Zsasz_call_BM_idles_5.wem
|
||||
Speech_SIDE_STORY_Zsasz_call_BM_idles_6.wem
|
||||
Speech_SIDE_STORY_Zsasz_call_BM_idles_7.wem
|
||||
Speech_SIDE_STORY_Zsasz_idles_1.wem
|
||||
Speech_SIDE_STORY_Zsasz_idles_2.wem
|
||||
Speech_SIDE_STORY_Zsasz_idles_3.wem
|
||||
Speech_SIDE_STORY_Zsasz_idles_4.wem
|
||||
Speech_SIDE_STORY_Zsasz_idles_5.wem
|
||||
Speech_SIDE_STORY_Zsasz_idles_6.wem
|
||||
Speech_SIDE_STORY_Zsasz_idles_7.wem
|
||||
Speech_SIDE_STORY_Zsasz_idles_8.wem
|
||||
Speech_SIDE_STORY_Zsasz_idles_9.wem
|
||||
Speech_SIDE_STORY_Zsasz_idles_10.wem
|
||||
Speech_SIDE_STORY_Zsasz_idles_11.wem
|
||||
Speech_SIDE_STORY_Zsasz_idles_12.wem
|
||||
Speech_SIDE_STORY_Zsasz_idles_13.wem
|
||||
Speech_SIDE_STORY_Zsasz_idles_14.wem
|
||||
Speech_SIDE_STORY_Zsasz_idles_15.wem
|
||||
Speech_SIDE_STORY_Zsasz_idles_16.wem
|
||||
Speech_SIDE_STORY_Zsasz_idles_17.wem
|
||||
Speech_SIDE_STORY_Zsasz_intro_1.wem
|
||||
Speech_SIDE_STORY_Zsasz_intro_2.wem
|
||||
Speech_SIDE_STORY_ZSASZ_SEEBM_1.wem
|
||||
Speech_SIDE_STORY_Zsasz_Z_Caught_1.wem
|
||||
Speech_SIDE_STORY_Zsasz_Z_Caught_2.wem
|
||||
14
docs/README.md
Normal file
14
docs/README.md
Normal file
@@ -0,0 +1,14 @@
|
||||
# Documentation
|
||||
|
||||
| document | content |
|
||||
|----------|---------|
|
||||
| [user-guide.md](user-guide.md) | installing and using Unheard City (screenshots) |
|
||||
| [decisions.md](decisions.md) | stack, dependencies/licenses, originals store, size rule, trade-offs |
|
||||
| [categories.md](categories.md) | group/category scheme and character fields |
|
||||
| [building.md](building.md) | building the editor and its installer (Windows, Linux) |
|
||||
| [formats/wad.md](formats/wad.md) | WWAD archives, grouped entries, BKPK stream packs |
|
||||
| [formats/wem.md](formats/wem.md) | WEM layout the game accepts, setup stripping, hash and decode-alloc formulas |
|
||||
| [reverse-engineering/decoder.md](reverse-engineering/decoder.md) | BatmanAC.exe decoder functions and addresses |
|
||||
| [reverse-engineering/findings.md](reverse-engineering/findings.md) | root cause, findings, in-game experiment log (incl. failed hypotheses) |
|
||||
| [../data/catalog/README.md](../data/catalog/README.md) | catalog CSV schema, adding languages |
|
||||
| [history/](history/) | the two mission briefs this project was built from |
|
||||
55
docs/building.md
Normal file
55
docs/building.md
Normal file
@@ -0,0 +1,55 @@
|
||||
# Building Unheard City
|
||||
|
||||
The build is a PyInstaller **one-folder** bundle: Python, Qt (incl. Qt Multimedia), libsndfile (libvorbis,
|
||||
FLAC, opus, mpg123) and soxr are all inside; nothing else is installed or needed at runtime. Build on each
|
||||
target OS (no cross-compiling).
|
||||
|
||||
## Windows (x86-64) - tested
|
||||
```
|
||||
py -3.14 -m venv .venv (Python 3.10-3.14)
|
||||
.venv\Scripts\python -m pip install -e .[app,dev]
|
||||
.venv\Scripts\python -m pytest -q
|
||||
.venv\Scripts\python tools\build_app.py
|
||||
```
|
||||
That one command does everything, offline. If `app/assets/` (git-ignored icon and wallpaper) is missing, it is
|
||||
made from `bac_logo_full.png` and `wallpaper.jpg` in `Downloads\Arkham City` (or `--art FOLDER`); without those
|
||||
images the build still works, with a plain icon and no wallpaper.
|
||||
Output: `dist\UnheardCity\UnheardCity.exe` and `dist\UnheardCity-<ver>-windows-amd64.zip`
|
||||
(~78 MB zip, ~190 MB unpacked). The build also writes `THIRD_PARTY_LICENSES.txt` (license texts come from the
|
||||
wheels and from `tools/licenses/`).
|
||||
|
||||
**Installer.** On Windows the folder (and zip) also contains `Install.cmd`, `install.ps1` and `uninstall.ps1`
|
||||
(sources in `tools/installer/`, version filled in by the build). It is a plain PowerShell installer, so no
|
||||
installer tool is needed to build it: per-user install to `%LOCALAPPDATA%\Programs\UnheardCity`, Start menu and
|
||||
optional desktop shortcut, an uninstall entry in Settings > Apps (`HKCU`), upgrade in place. User data in
|
||||
`%APPDATA%\UnheardCity` is kept on uninstall unless the user chooses otherwise. Unattended:
|
||||
`install.ps1 -Quiet -Desktop no [-Target DIR]`, `uninstall.ps1 -Quiet`.
|
||||
|
||||
Verify the bundle is self-contained (PATH stripped to Windows folders):
|
||||
```
|
||||
set PATH=C:\Windows\System32;C:\Windows
|
||||
dist\UnheardCity\UnheardCity.exe --smoke-test --report smoke.txt & type smoke.txt
|
||||
dist\UnheardCity\UnheardCity.exe --quit-after 8 --screenshot shot.png
|
||||
```
|
||||
|
||||
## Linux (x86-64) - procedure (not executed on the development machine)
|
||||
Use the oldest distribution you want to support (glibc compatibility), e.g. Ubuntu 22.04:
|
||||
```
|
||||
sudo apt install python3 python3-venv libxcb-cursor0 libxkbcommon-x11-0 libegl1 libpulse0
|
||||
python3 -m venv .venv && . .venv/bin/activate
|
||||
pip install -e .[app,dev]
|
||||
QT_QPA_PLATFORM=offscreen pytest -q
|
||||
python tools/build_app.py # -> dist/UnheardCity-<ver>-linux-x86_64.tar.gz
|
||||
QT_QPA_PLATFORM=offscreen dist/UnheardCity/UnheardCity --smoke-test --game /path/to/game
|
||||
```
|
||||
Notes:
|
||||
- The `soundfile` and `soxr` wheels for manylinux bundle libsndfile/libsoxr; the PySide6 wheels bundle Qt and
|
||||
Qt Multimedia's FFmpeg backend (PulseAudio/PipeWire output). The target desktop only needs the usual X11/
|
||||
Wayland and PulseAudio/PipeWire client libraries that every desktop distribution has.
|
||||
- Game folders under Proton are found automatically in `~/.steam/steam`, `~/.local/share/Steam`, Flatpak
|
||||
(`~/.var/app/com.valvesoftware.Steam/...`) and Snap Steam libraries, including extra libraries from
|
||||
`libraryfolders.vdf`. The game language is read from the Proton prefix
|
||||
(`steamapps/compatdata/200260/pfx/drive_c/users/steamuser/Documents/WB Games/...`).
|
||||
- The in-game test harness (`ackit verify --ingame`) is Windows-only; everything else is portable.
|
||||
|
||||
A GitHub Actions workflow that builds both platforms is in `.github/workflows/build.yml`.
|
||||
64
docs/categories.md
Normal file
64
docs/categories.md
Normal file
@@ -0,0 +1,64 @@
|
||||
# Categories and characters
|
||||
|
||||
Implemented in `ackit/classify.py`; used both when building the shipped catalog (`tools/build_catalog.py`)
|
||||
and when a game build is not in the catalog (runtime scan), so every language/build gets the same scheme.
|
||||
|
||||
## Groups and categories
|
||||
Every sound has a **group** and a finer **category**, derived from its Wwise name (from the community hash
|
||||
list; unnamed IDs show as hex). First matching rule wins:
|
||||
|
||||
| name starts with | group | category |
|
||||
|------------------|-------|----------|
|
||||
| `CHR_RadioScanner`, `PSO_CarRadio`, `Speech_RADIO`, `Speech_TV`, `Speech_CAMERAS`, `DIA_` | Speech | Radio & TV |
|
||||
| `Speech_TAPE_` | Speech | Interview tapes |
|
||||
| `Speech_INTERROGATION_` | Speech | Interrogations |
|
||||
| `Speech_CINEMATIC_`, `Speech_New_` | Speech | Cinematics |
|
||||
| `Speech_Boss_` / `Speech_BOSS_` | Speech | Boss fights |
|
||||
| `Speech_RIDDLER_`, `Speech_RIDDLES_` | Speech | Riddler |
|
||||
| `Speech_SIDE_` | Speech | Side missions |
|
||||
| `Speech_DLC_` | Speech | DLC |
|
||||
| `Speech_PRED_` | Speech | Predator chatter |
|
||||
| `Speech_COMBAT_`, `Speech_Bully_` | Speech | Combat barks |
|
||||
| `Speech_EMOTE_` | Speech | Efforts & emotes |
|
||||
| `Speech_REACT_` | Speech | Reactions |
|
||||
| `Speech_STREETS_`, `Speech_ZONES_`, `Speech_OUTCAST_`, `Speech_Vertical_` | Speech | Open-world chatter |
|
||||
| `Speech_CHAR_` | Speech | Character lines |
|
||||
| `Speech_CH_`, `Speech_SEQUENCE_`, `Speech_STEEL_`, `_MUSEUM_`, `_CHURCH_`, `_COURT_`, `_UNDER_`, `_GCPD_`, `_ZP1_` | Speech | Story missions |
|
||||
| `Speech_GAME_`, `Speech_MAP_`, `Speech_AUGMENTED_`, `Speech_FORENSICS_`, `Speech_TEMP_` | Speech | Gameplay & hints |
|
||||
| `NPC_` | Ambience | Crowds |
|
||||
| other `Speech_` | Speech | Other speech |
|
||||
| media of the `Music` / `Music_PDLC` stream packs, `Music*` | Music | Music / Music (DLC) |
|
||||
| `AMB_`, `PSA_` | Ambience | Ambience |
|
||||
| `WND_` | Ambience | Wind |
|
||||
| `VOL_`, `PVOL_` | Ambience | Room tone |
|
||||
| `SEQ_` | SFX | Scripted sequences |
|
||||
| `OBJ_` | SFX | Objects |
|
||||
| `WPN_` | SFX | Weapons |
|
||||
| `CHR_` (in SFX) | SFX | Character foley |
|
||||
| `Global` | SFX | Global |
|
||||
| anything else (e.g. unnamed hex IDs) | Unknown | Unknown |
|
||||
|
||||
Counts in the ITA catalog: Story missions 1993, Predator chatter 1796, Character lines 1537, Open-world chatter
|
||||
1295, Side missions 808, Combat barks 801, Efforts & emotes 616, Interview tapes 514, Riddler 360, Radio & TV 226,
|
||||
Gameplay & hints 199, DLC 167, Boss fights 150, Cinematics 150, Reactions 108, Interrogations 95. SFX archive:
|
||||
Music 389, Ambience 225, SFX 31, Unknown 6.
|
||||
|
||||
## Characters
|
||||
Two separate fields, because Wwise names describe a *scene*, not the speaker of each variation (a conversation
|
||||
alternates speakers between `NAME_1`, `NAME_2`, ...):
|
||||
|
||||
- **character** - the verified speaker, only from the curated lists in `data/catalog/source/<lang>/*.txt`
|
||||
(one file per character, listening-based). Mapped by sound name, so labels made on the Italian files apply to
|
||||
every language (the five language WADs have identical names and variation counts). Display names:
|
||||
Calendar -> Calendar Man, Croc -> Killer Croc, Freeze -> Mr. Freeze, Ivy -> Poison Ivy, Madhatter -> Mad
|
||||
Hatter, Quinn -> Harley Quinn, Ras -> Ra's al Ghul, Strange -> Hugo Strange, Talia -> Talia al Ghul,
|
||||
Twoface -> Two-Face, Vicky -> Vicki Vale, Zsazz -> Victor Zsasz, Unknown -> Unknown (speaker checked but not
|
||||
identified). A line in two lists gets both ("A; B").
|
||||
- **characters_inferred** - characters and roles *mentioned by the name* (token rules in `classify._TOKENS`,
|
||||
e.g. `BM`/`BATMAN` -> Batman, `PENG` -> Penguin, `TF` -> Two-Face, `THUG*`/`GENTHUG*` -> Thug, `TYGER` ->
|
||||
TYGER guard; interview tapes add Hugo Strange). A hint for the ~7,500 lines without a verified speaker,
|
||||
never shown as the speaker. In the editor the character filter matches verified speakers, and optionally
|
||||
inferred ones.
|
||||
|
||||
Coverage (every language): 3,366 sounds with a verified speaker, 5,930 more with inferred characters, 1,520 with
|
||||
neither (mostly predator/efforts from unnamed thugs).
|
||||
107
docs/decisions.md
Normal file
107
docs/decisions.md
Normal file
@@ -0,0 +1,107 @@
|
||||
# Decisions
|
||||
|
||||
## 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.
|
||||
- **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.
|
||||
- **Distribution:** PyInstaller one-folder build per platform. It bundles Python, Qt and all native libraries;
|
||||
the user unzips and runs `UnheardCity(.exe)`, or on Windows double-clicks `Install.cmd` (see below). Nothing
|
||||
else is installed. Alternatives considered:
|
||||
Tauri/Rust or C# would mean rewriting the verified codec/injector (high risk, no benefit); a local web UI
|
||||
would still need a bundled runtime and makes audio recording harder.
|
||||
|
||||
## 2. No external programs at runtime
|
||||
The first toolkit used ffmpeg (decoding inputs and libvorbis encoding) and vgmstream (checks). Both are gone:
|
||||
- **Encoding:** `libsndfile` (via the `soundfile` wheel) contains libvorbis 1.3.7. Tested: it yields exactly the
|
||||
same codebook compatibility and the same game setups as ffmpeg's libvorbis (findings 12). Encoding is in-process.
|
||||
- **Decoding game WEMs:** ackit's own WEM -> Ogg converter (the game's codebooks) + libsndfile.
|
||||
- **Input formats:** libsndfile reads WAV, FLAC, OGG/Vorbis/Opus, MP3, AIFF and more. M4A/AAC/WMA would need
|
||||
ffmpeg: an ffmpeg found on the system is used if present, otherwise the user gets a clear "convert to WAV/MP3"
|
||||
message. Bundling ffmpeg (~80-100 MB, GPL/LGPL build care) for rare formats was judged not worth it.
|
||||
- **Resampling:** `soxr` (LGPL-2.1).
|
||||
So there are **no vendor binaries to fetch** (no `tools/fetch_vendor` script was needed): every native
|
||||
dependency comes from version-pinned PyPI wheels (which embed libsndfile, libvorbis, libogg, FLAC, opus, mpg123,
|
||||
Qt and Qt Multimedia's FFmpeg backend) that PyInstaller bundles, resolved relative to the app (never `PATH`).
|
||||
`tools/collect_licenses.py` writes `THIRD_PARTY_LICENSES.txt` into each build from the wheels' own license files
|
||||
plus official texts kept in `tools/licenses/` (sources in SOURCES.txt; no network at build time). Verified: the packaged Windows build runs
|
||||
its decode/encode/inject smoke test with `PATH` reduced to `C:\Windows` (no ffmpeg, vgmstream or Python).
|
||||
**Licenses:** PySide6/Qt LGPL-3.0 (dynamic linking, unmodified Qt shared libraries in the one-folder build,
|
||||
source at https://code.qt.io), libsndfile LGPL-2.1, mpg123 LGPL-2.1, soxr LGPL-2.1, libvorbis/libogg/FLAC/opus
|
||||
BSD, numpy BSD, Python PSF. One-folder (not one-file) builds keep the LGPL libraries replaceable.
|
||||
The only Windows-only part is the in-game test harness (`ackit verify --ingame`, uses WinDbg/cdb and WASAPI
|
||||
loopback); it is a developer tool and not part of the editor.
|
||||
|
||||
## 3. Where originals live (revert safety)
|
||||
- **Originals store** per game folder: `<user data>/UnheardCity/stores/<id>/` (Windows `%APPDATA%`, Linux
|
||||
`~/.local/share/unheardcity`), id = hash of the game path; configurable (`--store`, editor settings).
|
||||
Before a sound is first changed, its original WEM bytes (SHA-256-named blob) and metadata are saved;
|
||||
the applied replacement is saved too. **Revert** re-injects the stored original; **Revert all** does this for
|
||||
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.
|
||||
- **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
|
||||
first. Several sounds of one WAD are written in one pass. A locked WAD (game running) gives a clear message.
|
||||
- **Changed underneath:** each sound's state is computed from its bytes: original / modified (ours) / lost
|
||||
(ours was replaced by the original, e.g. Steam "verify") / changed (neither: update or another tool) /
|
||||
foreign (never touched by us, but not the catalog's original checksum). Writing over changed/foreign sounds
|
||||
requires explicit confirmation; the catalog is used only when the WAD fingerprint matches.
|
||||
|
||||
## 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;
|
||||
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. (Rewriting `.upk` packages would lift the
|
||||
limit but means LZO recompression of large engine packages: out of scope, high risk.)
|
||||
|
||||
## 5. Catalog
|
||||
- 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.
|
||||
- Category scheme: `docs/categories.md`.
|
||||
|
||||
## 6. Repository cleanup choices
|
||||
- Windows installer: a per-user PowerShell installer shipped in the zip (`Install.cmd`) instead of Inno Setup or
|
||||
MSI, so building needs no extra tool; no admin rights, Start menu shortcut, Settings > Apps uninstall entry.
|
||||
The zip still runs without installing.
|
||||
- One entry point: `ackit` (CLI) and `UnheardCity` (GUI). The `.bat` launchers and the `mods/` folder
|
||||
flow were removed (`ackit apply <folder>` and mod packages replace them).
|
||||
- The user's third-party downloads that sat in `tools/` and `repositories/` (quickbms, Ravioli, AbdAshes' guide,
|
||||
vgmstream/ww2ogg/revorb sources) were moved to the git-ignored `external/` folder, since `tools/` now holds
|
||||
build scripts only.
|
||||
- Experiment scripts and outputs were deleted; they are preserved in git tag `pre-cleanup`.
|
||||
- The old `_backup/` (2.3 GB full copies) was deleted on 2026-10-08 with the game verified original; the originals
|
||||
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
|
||||
(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
|
||||
window stays responsive. Errors are shown as plain sentences (game running/locked file, wrong folder,
|
||||
unsupported audio, too long, changed outside the tool).
|
||||
- No network access and no telemetry. User notes/tags/favorites live in the user data folder.
|
||||
- One look: neutral dark greys (no blue tint), a single dark red accent (#b0242c, theme and icon), solid panels. Icons are vector drawings in
|
||||
`app/icons.py` (no emoji, no icon fonts, no image files), so they look the same on every platform.
|
||||
- **Artwork:** the app icon (the Arkham City logo bat in flat black over a red audio waveform of rounded bars, on a
|
||||
white rounded tile; shown only in the window/taskbar and the toolbar corner) and the default background wallpaper come from images supplied by the user.
|
||||
`tools/make_assets.py <folder>` turns `bac_logo_full.png` + `wallpaper.jpg` into `app/assets/` (app_icon.png,
|
||||
app_icon.ico, wallpaper.jpg), which the build bundles. The folder is git-ignored because it is Warner Bros.
|
||||
artwork: keep builds that contain it for personal use. Without the assets the app falls back to a plain
|
||||
waveform icon and no wallpaper.
|
||||
- Background: the wallpaper sits only behind the filter panel, scaled to the panel height so its left part fills
|
||||
the panel, under a light veil. Settings -> Background toggles it and sets its strength (default 10 %).
|
||||
- Loading feedback stays where the work is, never modal: a spinner inside the waveform being loaded (sound or
|
||||
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. Testing limitations
|
||||
- No Linux machine, WSL distribution or container runtime is available on the development PC. Linux support is
|
||||
covered by portable code (no Windows-only calls outside `ackit/verify`), path handling for Steam/Flatpak/
|
||||
Proton, unit tests that run on any OS, and a documented build procedure; the Linux package could not be
|
||||
executed here.
|
||||
70
docs/formats/wad.md
Normal file
70
docs/formats/wad.md
Normal file
@@ -0,0 +1,70 @@
|
||||
# Arkham City WAD / BKPK container format
|
||||
|
||||
All values little-endian. Verified by byte-identical round trips of every shipped WAD
|
||||
(INT/ITA/FRA/DEU/ESN LOC WADs and `SFX\17705D3E.wad`) and every BKPK pack.
|
||||
|
||||
## WWAD (`*.wad`)
|
||||
|
||||
| off | type | meaning |
|
||||
|------|------|---------|
|
||||
| 0x00 | 4s | `WWAD` |
|
||||
| 0x04 | u32 | version `0x01020000` |
|
||||
| 0x08 | u32 | entry count N |
|
||||
| 0x0C | u32 | table offset (always `0x1C`) |
|
||||
| 0x10 | u32 | table size (`12*N`) |
|
||||
| 0x14 | u32 | data start (header area size; `0x8000` LOC, `0x1000` SFX) |
|
||||
| 0x18 | u32 | data size (`file size - data start`) |
|
||||
| 0x1C | N x {u32 hash, u32 size, u32 offset} | |
|
||||
|
||||
Rules:
|
||||
- Table sorted by `hash` interpreted as **signed** int32. Payloads stored in table order.
|
||||
- Each payload starts at the next **0x1000-aligned** offset after the previous one.
|
||||
- `size` = payload length rounded up to **0x800**; padding is zero.
|
||||
- File ends 0x1000-aligned; header area after the table is zero.
|
||||
- `hash` = Wwise media/file ID (names: `hashlist.txt`, e.g. `CHR_RadioScanner`, `AMB_Steel_B3`, `Music`).
|
||||
|
||||
Payload kinds:
|
||||
- **LOC WADs**: each entry is a group of WEMs, the variations of one line (ITA: 10816 WEMs in
|
||||
2464 entries; e.g. `CHR_RadioScanner` holds 91). Each WEM starts at the next 0x800 boundary after
|
||||
the previous one (zero padding); 3 entries pack two WEMs back to back. Addressed `HASH:i`.
|
||||
- **SFX WAD**: 79 grouped ambience/effect entries (260 WEMs) plus 4 **stream packs** (below).
|
||||
|
||||
Where the game gets the inner offsets: the entry IDs appear next to `BKPK` headers inside the
|
||||
LZO-compressed UE3 packages (e.g. `BmGame.upk`, `OW.upk`, `Church_B1_*.upk`). Those embedded BKPK
|
||||
tables give each WEM's offset **and size**. ackit does not rewrite `.upk` files, so these WEMs are
|
||||
replaced **in place, no larger than the original** (zero-filled to the original size), and the WAD
|
||||
layout never changes. The user's earlier quickbms-reimport mod did the same and was loaded by the game.
|
||||
Only the 4 SFX stream packs, whose BKPK index is a loose `.bnk`, can change size.
|
||||
|
||||
## BKPK stream packs (`SFX\<ID>.bnk` + SFX WAD entry `<ID>`)
|
||||
|
||||
`EDF036D6` (Music, 314 WEMs, incl. the intro/front-end music `393BC53A`), `E8F980BE` (Music_PDLC, 75),
|
||||
`57572DAC` (Global, 1), `FCA4F778` (1). The `.bnk` file is an index + an embedded soundbank; the WEM
|
||||
data lives in the SFX WAD entry with the same ID.
|
||||
|
||||
| off | meaning |
|
||||
|------|---------|
|
||||
| 0x00 | `BKPK` |
|
||||
| 0x04 | version `0x01020000` |
|
||||
| 0x08 | pack id (== WAD entry hash) |
|
||||
| 0x10 | media count N |
|
||||
| 0x18 | table size `12*N` |
|
||||
| 0x1C | table offset `0x48` |
|
||||
| 0x20 | total stream size (== WAD entry size) |
|
||||
| 0x2C | end of table (`0x48 + 12*N`) |
|
||||
| 0x38 | embedded bank size |
|
||||
| 0x3C | embedded bank offset (0x800 or 0x1000; a normal BKHD/HIRC/STID bank) |
|
||||
| 0x48 | N x {u32 media id, u32 size, u32 offset in the WAD entry} |
|
||||
|
||||
Rules: table sorted by media id; each media starts at the next **0x800-aligned** offset; total =
|
||||
end rounded up to 0x800. Padding is mostly zero (a few non-zero bytes in `EDF036D6`; the tool keeps
|
||||
original bytes where nothing moved).
|
||||
|
||||
Replacing a pack media (`ackit` target `EDF036D6/393BC53A`) rewrites the pack table (`.bnk`), the
|
||||
WAD entry payload, and the WAD table/offsets. Verified in-game (NOTES.md, E2).
|
||||
|
||||
## Regular soundbanks (`SFX\*.bnk`, bank version 56)
|
||||
|
||||
BKHD / DIDX {id, offset, size} / DATA / HIRC / STID. In-memory WEMs (DATA) are 16-byte aligned.
|
||||
`ackit/bnk.py` can read/rebuild them, but most front-end sounds come from banks embedded in UE3
|
||||
packages (`.upk`), which this toolkit does not modify.
|
||||
82
docs/formats/wem.md
Normal file
82
docs/formats/wem.md
Normal file
@@ -0,0 +1,82 @@
|
||||
# WEM format accepted by Arkham City (Wwise 2011.1, Vorbis)
|
||||
|
||||
Derived from the game's WEMs; `ackit verify` / `tests/test_realgame.py` rebuild them byte-for-byte from these rules
|
||||
alone (all 10816 ITA WEMs; in SFX only the 4 looping music tracks with loop points differ).
|
||||
|
||||
## RIFF layout
|
||||
`RIFF` size `WAVE`, then `fmt ` (0x42 bytes), optional `cue `/`LIST` (37 files), `data`.
|
||||
Odd-sized `data` chunks are **not** padded.
|
||||
|
||||
### `fmt ` (0x42)
|
||||
| off | type | value |
|
||||
|-----|------|-------|
|
||||
| 0x00 | u16 | 0xFFFF (Wwise Vorbis) |
|
||||
| 0x02 | u16 | channels (1,2,3,4,5,6 seen) |
|
||||
| 0x04 | u32 | sample rate |
|
||||
| 0x08 | u32 | avg bytes/s = `(data_len - seek_size) * rate // total_frames` |
|
||||
| 0x0C | u16,u16 | 0, 0 |
|
||||
| 0x10 | u16 | cbSize 0x30 |
|
||||
| 0x12 | u16 | valid bits (16 or 24 = source bit depth, informational) |
|
||||
| 0x14 | u32 | channel mask (mono 0x4, stereo 0x3, quad 0x33, 5.0 0x37, 5.1 0x3F) |
|
||||
| 0x18 | 42 B | AkVorbisInfo (below) |
|
||||
|
||||
### AkVorbisInfo (fmt+0x18)
|
||||
| off | type | field | rule |
|
||||
|-----|------|-------|------|
|
||||
| 0x00 | u32 | dwTotalPCMFrames | sample count |
|
||||
| 0x04 | u32 | dwLoopStartPacketOffset | `2 + setup_len` (relative to end of seek table) |
|
||||
| 0x08 | u32 | dwLoopEndPacketOffset | `data_len - seek_size` |
|
||||
| 0x0C | u16 | uLoopBeginExtra | 0 |
|
||||
| 0x0E | u16 | uLoopEndExtra | = uLastGranuleExtra |
|
||||
| 0x10 | u32 | dwSeekTableSize | bytes (4 per entry) |
|
||||
| 0x14 | u32 | dwVorbisDataOffset | `seek_size + 2 + setup_len` |
|
||||
| 0x18 | u16 | uMaxPacketSize | max audio packet payload |
|
||||
| 0x1A | u16 | uLastGranuleExtra | `sum(packet samples) - total_frames` (< blocksize) |
|
||||
| 0x1C | u32 | dwDecodeAllocSize | see "decode alloc" — **must be right** (too small = NULL write crash) |
|
||||
| 0x20 | u32 | dwDecodeX64AllocSize | `alloc + 8*nbooks + 40 + 52*nfloors` (fit; unused by 32-bit game) |
|
||||
| 0x24 | u32 | uHashCodebook | **FNV-1 32** of the stripped setup bytes; codec-setup cache key |
|
||||
| 0x28 | u8,u8 | uBlockSizes | log2 short, log2 long |
|
||||
|
||||
## `data` chunk
|
||||
```
|
||||
[seek table: N x {u16 granule delta, u16 byte delta}]
|
||||
[u16 setup_len][stripped setup packet]
|
||||
[u16 len][audio packet] ...
|
||||
```
|
||||
- Seek points: walk packets; point at the first packet whose *end* granule >= previous point + interval
|
||||
(8192 for SFX/speech; music uses 1024 or other per-sound values). Byte offsets are relative to the
|
||||
setup start (end of seek table).
|
||||
- No identification/comment headers (rebuilt from fmt).
|
||||
|
||||
## Stripped setup ("external codebooks")
|
||||
Standard Vorbis setup with: no `\x05vorbis` header; codebook count (8b), then each codebook as a
|
||||
**10-bit ID** into the built-in library; no time-domain section; floor type field removed (always 1);
|
||||
residue type 2 bits; mapping type removed; mode window/transform type removed. See
|
||||
`ackit/wwvorbis.py:transcode_setup` (bidirectional).
|
||||
|
||||
Codebook library: the game's table (runtime `0x0158E748`, 598 books) is byte-identical to ww2ogg's
|
||||
standard `packed_codebooks.bin` (`ackit/data/game_codebooks.bin`). **Wwise 2011.2.x uses the aoTuV 6.03
|
||||
library instead** — same IDs, different books — which is why files from the newer encoder decode to
|
||||
garbage/silence in this game.
|
||||
|
||||
Stock libvorbis (1.3.7, in ffmpeg and in the libsndfile wheels ackit uses) setups map onto this library for all tested rates with 1/2/4/5
|
||||
channels, and for 6 channels up to 32 kHz (its 5.1 books at 44.1/48 kHz are newer than the game).
|
||||
At 24 kHz q>=5 needs a book not in the library. The encoder probes and picks a compatible quality.
|
||||
|
||||
## Audio packets
|
||||
- Blocksizes differ (e.g. 256/2048): "modified" packets — packet-type bit and the long-window
|
||||
prev/next flags are removed (mode number stays first).
|
||||
- Equal blocksizes (512/512, 1024/1024): standard Vorbis packets (type bit kept).
|
||||
- 2-byte size header, no granule.
|
||||
|
||||
## Decode alloc (dwDecodeAllocSize)
|
||||
`0x30 + parser allocations + 4*nbooks + 8`, every allocation rounded up to 4 bytes. Parser
|
||||
allocations (Tremor-lowmem derived, `ackit/allocsize.py`):
|
||||
- codebooks: `nbooks*0x3C` + per book: decode table (`used<2` or nodeb==4: `used*8+4`, else
|
||||
`align4((used*(leafw+1)-2)*nodeb)`) + for dec_type 2 the quantized values (`align4(quantvals)` or
|
||||
`align4(2*quantvals)`).
|
||||
- floors: `nfloors*0x24` + per floor `a4(parts) + a4(11*(maxclass+1)) + a4(2*(posts+2)) + a4(posts+2) + 2*a4(posts)`.
|
||||
- residues: `nres*0x1C` + per residue `a4(ncls) + 8*ncls`.
|
||||
- mappings: `nmaps*0x14` + `a4(2*coupling_steps)` + `a4(channels)` if submaps>1 + `a4(2*submaps)`.
|
||||
- modes: `a4(2*nmodes)`.
|
||||
Exact on all 24 setups the game ships; in-game test E10 used a new setup with computed values.
|
||||
92
docs/history/mission1_prompt.md
Normal file
92
docs/history/mission1_prompt.md
Normal file
@@ -0,0 +1,92 @@
|
||||
# MISSION: Arkham City audio modding toolchain (autonomous)
|
||||
|
||||
You are working fully autonomously. I will not answer questions or intervene. When something is ambiguous, pick the most reasonable option, record the assumption in `NOTES.md`, and continue. Never stop to ask me for permission or clarification. Stop only when a success criterion below is met, when a stop condition is hit, or when usage limits force a pause (see "Limits").
|
||||
|
||||
## Goal
|
||||
|
||||
Make it possible to replace audio in **Batman: Arkham City (PC)** with custom audio in a simple, repeatable way. Deliverables:
|
||||
|
||||
1. **Encoder tool**: converts an arbitrary WAV/OGG/MP3 into a WEM file the game actually accepts and plays (correct RIFF/Wwise chunks, Vorbis stream and codebooks as the game's Wwise version expects).
|
||||
2. **Injector tool**: replaces a given WEM inside the game's WAD container, tracking file sizes and alignment, and updating all offset/size tables so the container stays valid. Must be reversible (restore from backup).
|
||||
3. **A short README** documenting the WAD structure, WEM format findings, and usage, so a non-expert can replace a sound with one command.
|
||||
|
||||
Success = a custom sound I supply plays correctly in-game in place of the original, with no crash, verified by your harness (see "Verification").
|
||||
|
||||
## What is already known (do not redo this work)
|
||||
|
||||
- Game was built with **Audiokinetic Wwise 2011.1.0**, which is unavailable. The closest available is **2011.2.2** (Internet Archive). Audio is a custom Wwise RIFF (`RIFF/WAVE`, `fmt`, `data`, etc.) containing Vorbis with **stripped headers and packed codebooks**.
|
||||
- Differences between 2011.1.0 and 2011.2.x exist, including **which Vorbis codebooks the encoder uses**. Header layout differences are also possible and not fully mapped.
|
||||
- Existing public tools (unpackers/repackers) work for extracting and repacking, but the resulting audio is incompatible: the game refuses to play it or crashes. Alternative codecs (PCM, etc.) were already tried and do not work.
|
||||
- I already made a **patch that stops the crash**: a code cave that patches the variable holding the number of bytes to allocate on a **bump allocator**. Re-derive details from my patch/notes in the repo if present; do not assume it is complete.
|
||||
- The game executable has **DRM/packing**: many functions are missing when the original exe is analyzed statically. A **WinDbg memory dump of the running executable** exposes them. Use the dumped exe for Ghidra analysis; use the live process for dynamic analysis.
|
||||
- Useful references: **vgmstream** and **ww2ogg** (Wwise RIFF parsing, packed codebook handling, codebook files such as `packed_codebooks.bin` / aoTuV variants), **revorb**, and Wwise 2011.2.2 itself as a reference encoder.
|
||||
|
||||
Search the filesystem first for my existing patches, dumps, notes, and tools. Treat anything you find as input data, not as ground truth; verify before relying on it.
|
||||
|
||||
## Two approaches (pursue in this order, switch when evidence says so)
|
||||
|
||||
**A. Make the files compatible with the game.** Reverse engineer the game's WEM loader/Vorbis decoder to find exactly what it requires (header fields, codebook set/IDs, setup-packet format, packet/granule layout, block sizes, seek table, etc.). Then build an encoder that produces that format, likely by encoding with Wwise 2011.2.2 or libvorbis and rewriting headers/codebooks to match the game's expectations. Compare original game WEMs against Wwise 2011.2.2 output to find the differences.
|
||||
|
||||
**B. Patch the game binary** to accept more encodings (for example, make the decoder accept the 2011.2.x codebooks or headers, or add a path that supports another codec). Use this if A proves infeasible, or as a complement if it is simpler.
|
||||
|
||||
Decide by evidence. Document the reasoning in `NOTES.md`.
|
||||
|
||||
## Environment and tools
|
||||
|
||||
- Native Windows. Work in a git repo; commit often with meaningful messages.
|
||||
- Ghidra via MCP (static analysis, on the dumped exe).
|
||||
- **Steam-only game.** The game is installed through **Steam** and can only be run through Steam (Steam DRM/ownership check). Steam must be running and logged in. Launching the game exe directly may fail or exit; do not rely on it. Consequences you must handle:
|
||||
- Find the install folder (`steamapps\common\...`) and the game's `appmanifest_*.acf` to get the real App ID and exe name; do not guess them.
|
||||
- Launch the game via Steam (for example `steam://rungameid/<appid>` or `steam.exe -applaunch <appid>`), or via a Steam **launch option** that wraps the game with the debugger (`<debugger command> %command%`), so the Steam environment is inherited. Test which approach works and record it in `NOTES.md`. Use whichever allows attaching early enough to break during startup and audio loading.
|
||||
- **Never use Steam's "Verify integrity of game files"** and make sure automatic updates are off for this game, or Steam will overwrite the modified files. Your restore script, not Steam, is the way to restore.
|
||||
- Keep the backup of the game folder outside the Steam library, and write the restore script to work while Steam is running.
|
||||
- **Debugger (dynamic analysis): WinDbg is the baseline.** WinDbg is installed and is the default for dynamic analysis. First detect how it is installed (Store version `windbgx.exe`, install folder, any WinDbg MCP tool in your tool list) and record it in `NOTES.md`. Prefer a WinDbg MCP tool if one is connected; otherwise drive WinDbg from the command line with a startup command string, a log file, and debugger scripts (verify the switches actually work with the installed version before building the harness around them). It must run unattended: log to a file, set breakpoints/commands at startup, and terminate the debuggee and debugger on its own at the end. Attaching to the Steam-launched process (by name or PID) is a valid approach. Prefer scripted debugging with log files over interactive tool calls when it is cheaper for context.
|
||||
- **You are free to improve the workflow.** Any tool, library, script, or technique that speeds up or de-risks the modding process is welcome: other debuggers or tracing tools, API/file-access monitors (for example Process Monitor or Frida-style hooks), in-process hooks or injected DLLs that log or call the game's audio functions directly, emulation or unit-testing of the decoder in isolation, custom Ghidra scripts, format-fuzzing harnesses, or anything else you judge useful. Install what you need, keep installs minimal, and record each addition and why in `NOTES.md`.
|
||||
- Python for tooling. Available/installable: vgmstream-cli, ww2ogg, revorb, ffmpeg, libvorbis/oggenc, and Wwise 2011.2.2. Check what is installed; if a tool is missing and the network allows, install it; otherwise write a minimal replacement.
|
||||
- You may debug the running game autonomously: launch it, attach debuggers, set breakpoints, read memory, and kill it. Always work on a copy-restorable game directory.
|
||||
|
||||
## Rules of engagement
|
||||
|
||||
- **Back up first.** Before modifying anything in the game directory or WAD, create a verified backup and a one-command restore script. Never edit the only copy of anything. Every test run starts from a clean restore plus exactly one change.
|
||||
- Keep the game's original files byte-identical except for the intended injections.
|
||||
- Change one variable at a time when testing hypotheses, and log the result.
|
||||
- Prefer small, falsifiable experiments over long speculation. State a hypothesis, test it, record the outcome.
|
||||
- Do not spend effort on unrelated goals (graphics, gameplay, other codecs) unless needed.
|
||||
|
||||
## Verification (build this first)
|
||||
|
||||
Before deep reverse engineering, build an **automated test harness** script that: restores a clean game copy, injects a given WEM, launches the game through Steam with WinDbg attached and logging, triggers or loads the target sound in a deterministic way (for example, a save or a scripted entry point that plays it early), detects crash / hang / clean exit, captures debugger logs and exception info, and prints a PASS/FAIL summary.
|
||||
|
||||
Make "does it play" machine-checkable without a human listening. For example: breakpoint on the decoder's init/decode functions and log return values or error codes, check that decoded sample counts match expectations, and optionally capture audio output (loopback) and compare against the source with a similarity metric. Choose whatever is most reliable and document it. Also use vgmstream/ffmpeg decoding as an offline sanity check, remembering that offline success does not imply the game accepts the file.
|
||||
|
||||
## Suggested phases
|
||||
|
||||
1. Inventory: find existing work, tools, dumps, and notes. Set up the repo, `CLAUDE.md`, `NOTES.md`, backups, and restore script.
|
||||
2. Reverse the **WAD container**: header, file table, offsets, sizes, alignment, compression/encryption if any. Write a parser and a round-trip test (unpack, repack, byte-identical output).
|
||||
3. Build the test harness.
|
||||
4. Reverse the **WEM loading and Vorbis decoding path** in the dumped exe (and live process). Document exact format requirements.
|
||||
5. Compare original game WEMs with Wwise 2011.2.2 output and map every difference. Test hypotheses with the harness.
|
||||
6. Implement the encoder (approach A) or binary patch (approach B), whichever the evidence supports.
|
||||
7. Implement the injector with size/alignment/offset handling, including the case where the new file is larger or smaller than the original.
|
||||
8. Final end-to-end test with a custom sound, then write the README.
|
||||
|
||||
## Memory and context management
|
||||
|
||||
- `NOTES.md` is your persistent memory. After every meaningful finding or failed experiment, append: what you tried, the result, and the conclusion. Keep a short "Current status / Next steps" section at the top, rewritten as you go.
|
||||
- Record addresses, struct layouts, function names, and format specs in a `docs/` folder as you discover them, so you never have to rediscover them after a context reset.
|
||||
- At the start of every session or after a context reset, read `NOTES.md` first and continue from there.
|
||||
- Use subagents for bulk or mechanical work (large searches, log triage, batch experiments) to keep the main context small.
|
||||
|
||||
## Limits and efficiency
|
||||
|
||||
I am on a limited Claude Pro quota. Be economical: avoid dumping huge outputs into context (filter, grep, summarize logs), do not re-read large files unnecessarily, and delegate mechanical work to cheaper subagents. If you hit a usage limit, write a clear status in `NOTES.md` so work can resume cleanly.
|
||||
|
||||
## Stop conditions
|
||||
|
||||
- **Success:** the end-to-end test passes and the deliverables exist.
|
||||
- **Dead end:** if approach A and B have each had at least five distinct, well-documented failed hypotheses, stop and write a `REPORT.md` that lists what was proven, what was ruled out, the best remaining hypotheses, and the exact next experiments.
|
||||
- If any action risks something irreversible outside the project directory and the game directory, do not do it.
|
||||
|
||||
## Output at the end
|
||||
|
||||
Summarize in a few lines: what works, how to use the tools, and what remains. Everything else lives in the repo.
|
||||
109
docs/history/mission2_editor_prompt.md
Normal file
109
docs/history/mission2_editor_prompt.md
Normal file
@@ -0,0 +1,109 @@
|
||||
# MISSION: Clean up the repo, then build the Arkham City Audio Editor (autonomous)
|
||||
|
||||
You are working fully autonomously, exactly as in the previous mission. I will not answer questions or intervene. When something is ambiguous, pick the most reasonable option, record the assumption and reasoning in `NOTES.md`, and continue. Never stop to ask for permission or clarification. Stop only when the success criteria below are met, when a stop condition is hit, or when usage limits force a pause.
|
||||
|
||||
## Context
|
||||
|
||||
The previous mission succeeded: you reverse engineered the WAD container and the WEM format of **Batman: Arkham City (PC, Steam)**, built an encoder and an injector (the "ackit" tooling, in Python), and patched the game so modified audio plays. All of that is in this repository, together with many experiment scripts, notes, and session-resume markdown files.
|
||||
|
||||
Start by reading `NOTES.md`, `CLAUDE.md`, and the docs to re-learn the current state. Do not redo reverse engineering. The Steam rules still apply: the game runs only through Steam, never use "Verify integrity of game files", keep auto-updates off for this game, keep backups outside the Steam library, and keep game files byte-identical except for intended changes. WinDbg and your other tools remain available if you need to verify something.
|
||||
|
||||
## Part 1: Repository cleanup (do this first, then commit)
|
||||
|
||||
Goal: a small, clear repository where a newcomer can find everything.
|
||||
|
||||
1. **Safety net first.** Before deleting anything, commit the current state and create a git tag (for example `pre-cleanup`) so every deleted file is recoverable from history. Do not rewrite history.
|
||||
2. **Delete** every script and file that is not needed any more: one-off experiments, scratch analysis scripts, dead ends, duplicated tools, temporary outputs, large generated artifacts. Judge by whether the file is part of the final working pipeline or documents it.
|
||||
3. **Keep:**
|
||||
- `ackit` (the final toolkit), with only the code that matters: encoding, injection, WAD/WEM parsing, patching, and testing/verification. Merge duplicates, remove leftover and unused code, give modules clear names, and add short docstrings. Keep the tests, make sure they pass, and add tests where core behavior is uncovered (WAD round-trip, WEM encode/decode check, inject/revert).
|
||||
- **All documentation you wrote**: format specs, address maps, reverse-engineering notes, findings, failed hypotheses worth remembering. Move them into a tidy `docs/` structure, but do not delete or condense away information.
|
||||
- **The markdown files used to resume sessions** (`NOTES.md`, `CLAUDE.md`, and similar). Keep them, update them to describe the new layout, and keep a short "Current status / Next steps" section.
|
||||
4. **Simplify usage.** Provide one clear entry point for the toolkit, for example a single CLI (`ackit <command>`) with a few well-named subcommands (such as `info`, `list`, `extract`, `encode`, `inject`, `revert`, `verify`) and helpful `--help` text. Replace the multi-step manual procedures with one-command workflows where possible. Write a short, clear top-level `README.md`: what it is, install, quick start with 2-3 examples, and where things live.
|
||||
5. **Reorganize the layout** so each kind of thing has an obvious place. Suggested (adapt if you find better):
|
||||
```
|
||||
ackit/ Python package (core library + CLI)
|
||||
tests/ automated tests
|
||||
data/catalog/ CSV catalogs (one per language/WAD, see below)
|
||||
docs/ format specs, reverse-engineering notes, how-tos
|
||||
app/ the editor application (Part 2)
|
||||
tools/ build/packaging helper scripts only
|
||||
NOTES.md, CLAUDE.md, README.md
|
||||
```
|
||||
6. **Catalog.** My curated catalog of audio files (by character, mainly for the ITA language, though the idea applies to other language WADs too) is the source for names/characters. You had started converting it into CSV files. Locate the original catalog files and your CSV work. Finish converting everything into well-structured CSVs under `data/catalog/`, with a documented schema (see Part 2) and a short `data/catalog/README.md` explaining columns and how to add other languages. Do not lose or overwrite my original catalog files; keep them in a clearly named `data/catalog/source/` folder (unless they are too large or clearly not redistributable, in that case document where they are).
|
||||
7. Add a `.gitignore` so game assets, extracted audio, dumps, and build outputs never get committed. Do not commit any Arkham City game audio.
|
||||
8. Run the test suite and one real end-to-end check (inject a custom sound, confirm it plays in game through the existing harness if still available, and revert) to prove the cleanup did not break anything. Commit.
|
||||
|
||||
## Part 2: The Audio Editor application
|
||||
|
||||
Build a simple yet powerful desktop editor for the game's audio, working on **Windows and Linux** (on Linux the game runs through Steam/Proton, so the editor must accept any game folder path and handle Proton library paths; the editor itself does not need to launch the game).
|
||||
|
||||
### Technology decision (you decide, document it in `docs/decisions.md`)
|
||||
|
||||
Existing tools are Python. Pick the stack that best balances: working on Windows and Linux, easy installation for non-technical users, good audio playback/recording, reuse of `ackit`, and reasonable effort. Candidates: Python with a Qt GUI (PySide6) packaged with PyInstaller/Nuitka (bundles its own Python, no separate install), a web UI served locally and shown in a window, or a rewrite in another language (for example Rust/Tauri or C#). Prefer reusing `ackit` unchanged over rewriting it, unless there is a strong reason. Requirements for the decision:
|
||||
- Distribution must not require the user to install Python or other runtimes (bundle what is needed), or else provide a clearly simple one-step installer.
|
||||
- The encoder must work **without Wwise** at runtime and on Linux as well as Windows. If any part of the encoding pipeline depends on a Windows-only tool, replace that part with portable code, or ship the required pre-generated data (such as codebook data) with the app. Verify on both platforms as far as you can (you can test Linux behavior under WSL or a Linux container if available).
|
||||
- Audio I/O must be portable (for example `sounddevice`/PortAudio, `miniaudio`, or Qt Multimedia), with device selection.
|
||||
- **Bundle external tooling in the distribution.** If the app or `ackit` needs any external tool or binary (for example ffmpeg/ffprobe for decoding and converting, vgmstream-cli for decoding game WEMs, revorb, ww2ogg, oggenc/libvorbis, PortAudio or other native libraries), include it in the final dist build for each platform, so the user installs nothing else. Rules:
|
||||
- Prefer removing a dependency when a small portable library does the job (for example a Python decoder instead of a full ffmpeg), to keep the package small. Otherwise bundle the binary.
|
||||
- Ship per-platform binaries (Windows and Linux, x86-64 at least) in a `vendor/` or `bin/` folder inside the package, and resolve them at runtime relative to the app (never rely on `PATH`). Fall back to a system copy only if the bundled one is missing.
|
||||
- Download them from official/trusted sources in a build script (`tools/fetch_vendor.*`), pin versions, and verify checksums. Do not commit large binaries to git; fetch them at build time.
|
||||
- **Licenses:** check each tool's license (for example ffmpeg LGPL/GPL build variants; prefer an LGPL build without GPL-only components if sufficient). Include a `THIRD_PARTY_LICENSES` file with each license text, version, and source URL, and satisfy redistribution terms (for example, point to the source). Record the decisions in `docs/decisions.md`.
|
||||
- Test the packaged build on a machine/container **without** these tools on `PATH` to prove it is self-contained.
|
||||
|
||||
### Feature 1: Browser
|
||||
|
||||
- Load a game folder (and its WADs), remember the last path, and show a table of all audio entries with fast loading even for tens of thousands of entries (lazy loading, indexing/caching in a user cache folder; do not decode everything up front).
|
||||
- **Search** by name/ID, plus **filters** for category, character, language/WAD, and any other useful field. Combine them freely, with instant results as you type.
|
||||
- **Categories:** you decide a sensible category scheme (for example Speech, Music, SFX, Ambience, UI, Combat, Cinematic...). Derive them from the catalog and from the data itself (WAD/folder names, Wwise bank/event names, file size/duration heuristics, naming patterns), and document the scheme and the rules in `docs/`. Entries that cannot be classified go into an "Unknown" category and must remain searchable.
|
||||
- **Characters** come from my catalog CSVs. Entries not in the catalog still appear, with empty character.
|
||||
- Add other columns/filters you judge useful (duration, size, sample rate, channels, modified/original status, catalog notes or transcript text if present in the catalog, favorites/tags). Sorting on every column, multi-select, and a "modified only" filter.
|
||||
- Quick preview playback directly from the list (play/stop, keyboard-driven navigation), with a visible indicator for entries modified by the user.
|
||||
- Let the user add their own notes/tags per entry, stored in a user data file (not in the game folder).
|
||||
|
||||
### Feature 2: Single-file editor
|
||||
|
||||
Opening an entry shows an editor view with:
|
||||
- **Listen**: play original and current (modified) audio, waveform display, seek, volume, loop, and A/B compare between original and replacement.
|
||||
- **Substitute**: replace the audio from a file (WAV, OGG, MP3, FLAC, and whatever else ffmpeg or the chosen libs decode easily). Handle sample-rate/channel conversion, loudness/volume adjustment, and trimming. Warn clearly if the replacement is much longer/larger than the original, and make sure the injector handles any size correctly (offsets and alignment already solved in ackit; reuse it).
|
||||
- **Record**: record in the moment from a user-selected **audio input device** (device list, level meter, record/stop/re-record, preview before accepting, optional trim and normalize). Test recording logic with a virtual or file-based input where no hardware exists.
|
||||
- **Apply / Revert**: applying encodes with ackit and injects into the game WAD (with the safety rules from before: back up before first write, atomic/safe writes, never leave the WAD in a half-written state). **Revert** must restore the original audio of that entry, and there must also be "Revert all". Decide where originals live and justify it in `docs/decisions.md`. A good default: the first time an entry is modified, store its original bytes (with hash and location metadata) in an `originals` store; revert re-injects from it, so no full-WAD copy is required. Consider also keeping a pristine backup of the whole WAD as a fallback. Choose the location (user data folder vs. next to the game files) by what is most convenient and portable, for example a per-game-folder store with a configurable path, and make it survive app updates. Detect when the game files changed underneath (for example after a Steam update or verification) and warn the user safely instead of corrupting anything.
|
||||
- **Export/import mods:** let the user export their changes as a portable package (replacement audio plus a manifest) and import it on another machine or share it, so mods are not tied to one WAD state. Keep it simple.
|
||||
|
||||
### UX and quality bar
|
||||
|
||||
- Simple first: the primary flow (search, select, listen, replace/record, apply) must be obvious without reading docs. Power features stay available but unobtrusive.
|
||||
- Clear messages for errors (wrong game version, missing patch, unsupported audio, WAD locked by a running game, Steam path issues).
|
||||
- Do not add telemetry or network access, except where strictly needed (none expected).
|
||||
- Keep dependencies reasonable and pinned; keep the app responsive (long operations in background threads with progress and cancel).
|
||||
|
||||
## Verification (build it as you go)
|
||||
|
||||
- Unit/integration tests for the library; headless tests for the app logic (search/filter index, catalog loading, apply/revert, mod export/import). Use fixtures or synthetic WADs where real game data cannot be committed.
|
||||
- Automated GUI smoke tests (for example headless/offscreen Qt or a browser driver) that open the app, search, open an entry, replace audio from a file, apply, and revert, checking byte-level restoration of the original.
|
||||
- At least one **real end-to-end test against the actual game** using the existing harness: apply a replacement through the editor code path, confirm the game still starts and plays it, then revert and confirm the WAD is byte-identical to the original.
|
||||
- Build the distributable packages for the platform you are on, and verify as far as possible that the Linux package starts and works (container or WSL). Document exactly how to build for the other platform.
|
||||
|
||||
## Deliverables
|
||||
|
||||
1. Cleaned repository with the structure above, passing tests, and updated `NOTES.md`/`CLAUDE.md`/docs.
|
||||
2. Simplified `ackit` CLI and top-level `README.md`.
|
||||
3. Catalog CSVs and schema documentation.
|
||||
4. The editor app source in `app/`, with build scripts in `tools/`, plus a self-contained packaged build for the current platform (bundled external tools included, with `THIRD_PARTY_LICENSES`) and instructions to build for Windows and Linux.
|
||||
5. `docs/decisions.md` (stack, category scheme, originals store, trade-offs) and a user guide with a few screenshots (headless screenshots are fine).
|
||||
|
||||
## Working rules (unchanged from before)
|
||||
|
||||
- `NOTES.md` is your persistent memory: log progress, decisions, and failed attempts; keep "Current status / Next steps" at the top. After a context reset or a usage pause, read it first and continue.
|
||||
- Commit often with meaningful messages. Back up before touching game files, and test on restorable copies.
|
||||
- Be economical with quota: filter and summarize outputs, avoid re-reading large files, delegate mechanical or bulk work to subagents, and work in stages (cleanup first, then library changes, then browser, then editor, then packaging) so partial progress is always usable.
|
||||
- Order of priority if time or quota is short: (1) cleanup, (2) browser with search and filters, (3) single-file editor with substitute and revert, (4) recording, (5) mod export/import, (6) packaging polish. Never sacrifice the revert safety for features.
|
||||
|
||||
## Stop conditions
|
||||
|
||||
- **Success:** all deliverables exist and the verification tests pass, including the real game end-to-end test.
|
||||
- **Blocked:** if a requirement cannot be met (for example, the encoder truly needs a Windows-only component), implement the best alternative, document the limitation clearly in `docs/decisions.md` and `README.md`, and continue with the rest. Write `REPORT.md` at the end with what works, what does not, and the next steps.
|
||||
- Do not take irreversible actions outside the project and game folders.
|
||||
|
||||
## Output at the end
|
||||
|
||||
A few lines: what was cleaned up, what the app can do, how to run and build it, and known limitations. Everything else lives in the repo.
|
||||
BIN
docs/img/browse.png
Normal file
BIN
docs/img/browse.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 139 KiB |
BIN
docs/img/editor.png
Normal file
BIN
docs/img/editor.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 48 KiB |
BIN
docs/img/main.png
Normal file
BIN
docs/img/main.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 139 KiB |
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user