"""Step 8: build the EML mod into the repository (mod/: the result that is committed). usage: mod then `python install.py` copies it into the game and dry-runs it The mod replaces each note's EN sound (the Russian slot does not exist in VoiceoverResource: its fixed languages are EN/DE/FR/ES/ZH): SoundResource.streamedData points to the new QOA content, frameCount and duration follow it, VoiceoverResource.durations[EN] too (10 ms units). The game files are not touched: EML (dbghelp.dll proxy) patches enshrouded.kfc at launch and keeps enshrouded.kfc.bak (emm restore). """ import json import pathlib import shutil from . import __version__ from .config import ROOT, load LUA = '''local lines = require("lines") local ok, fail = 0, 0 for _, l in ipairs(lines) do local done, err = pcall(function() local snd = game.assets.get_resource(l.sound, "keen::SoundResource", 0) local d = snd.data local content = game.assets.create_content(io.read("data/" .. l.sound .. ".qoa")) d.streamedData.hash = content d.frameCount = l.frames d.framesPerSecond = 48000 d.duration.value = math.floor(l.seconds * 1e9) local vo = game.assets.get_resource(l.vo, "keen::VoiceoverResource", 0) vo.data.durations[1] = math.floor(l.seconds * 100) end) if done then ok = ok + 1 else fail = fail + 1; print("[ru_voice] FAIL " .. l.name .. ": " .. tostring(err)) end end print(("[ru_voice] replaced %d notes, %d failed"):format(ok, fail)) ''' def main(): cfg = load() notes = json.load(open(cfg.w("notes.json"), encoding="utf-8")) metas = [json.load(open(cfg.w("qoa", f"{n['k']}.json"))) for n in notes if cfg.w("qoa", f"{n['k']}.json").exists()] if len(metas) < len(notes): print(f"WARNING: {len(metas)} of {len(notes)} notes are assembled; the others stay English") mod = pathlib.Path(cfg.get("mod", "dir") or ROOT / "mod" / cfg.get("mod", "id", "ru_voice_notes")) if mod.exists(): shutil.rmtree(mod) (mod / "src").mkdir(parents=True) (mod / "data").mkdir() for m in metas: shutil.copy(cfg.w("qoa", f"{m['sound']}.qoa"), mod / "data" / f"{m['sound']}.qoa") lines = ["-- generated by ensvoice: one entry per voiced note", "return {"] lines += [f' {{ sound = "{m["sound"]}", vo = "{m["vo"]}", frames = {m["frames"]}, seconds = {m["seconds"]:.6f}, ' f'name = "{m["name"]}" }},' for m in metas] (mod / "src" / "lines.lua").write_text("\n".join(lines + ["}"]) + "\n", encoding="utf-8") (mod / "src" / "mod.lua").write_text(LUA, encoding="utf-8") json.dump(dict(id=cfg.get("mod", "id", "ru_voice_notes"), name=cfg.get("mod", "name", "RU voice: notes"), version=cfg.get("mod", "version", __version__), capabilities=["patch", "export"], description="Russian narration for the voiced notes (Qwen3-TTS ru-stress, narrator clone, " "automatic QC). Replaces the English voiceover slot.", authors=cfg.get("mod", "authors", [])), open(mod / "mod.json", "w"), indent=2) mb = sum(f.stat().st_size for f in (mod / "data").iterdir()) / 2 ** 20 print("mod", mod, len(metas), "notes", round(mb, 1), "MB; install: python install.py") if __name__ == "__main__": main()