Initial commit

This commit is contained in:
2026-10-09 02:48:24 +02:00
commit e05d973a8d
133 changed files with 101507 additions and 0 deletions

120
ackit/modpack.py Normal file
View 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