"""Install (or remove) the Russian narration mod. Python 3.11+, standard library only, Windows. usage: python install.py [--game ] install EML + the mod, dry run python install.py --uninstall [--game ...] restore the game files, remove EML + the mod What it does: 1. finds the game (Steam library folders) unless --game is given; refuses while the game is running; 2. downloads EML from github.com/Brabb3l/kfc-parser release v0.2.0 into tools/ (dbghelp.dll, emm.exe, kfc-parser.exe) and checks their SHA-256; 3. copies dbghelp.dll into the game folder (the EML proxy: it applies mods when the game starts, keeps enshrouded.kfc.bak and patches enshrouded.kfc, never the original .dat files) and mod/ru_voice_notes into /mods/; 4. dry run: emm.exe run --export must print "[ru_voice] replaced 525 notes, 0 failed". The mod plays the Russian takes in place of the English voiceover of the 525 voiced notes: keep the game's voice language English. """ import hashlib import os import pathlib import re import shutil import subprocess import sys import urllib.request ROOT = pathlib.Path(__file__).resolve().parent MOD = "ru_voice_notes" REL = "https://github.com/Brabb3l/kfc-parser/releases/download/v0.2.0/" SHA256 = { # release v0.2.0 assets "dbghelp.dll": "038b7a1787026007a68c82a6adeb8e79c2e644f936d4cd00f8b0db206a2349fa", "emm.exe": "b1133f6b8ea7000596cc7710901bd8307b48165b0e650c0487a611d90ea49305", "kfc-parser.exe": "b5384a926885a0b59da0a2475ff1fd1c2a0679156e2dbcdb52d1899aabc85b75", } def find_game(): roots = [] try: import winreg for hive, key in ((winreg.HKEY_CURRENT_USER, r"Software\Valve\Steam"), (winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\WOW6432Node\Valve\Steam")): try: with winreg.OpenKey(hive, key) as k: for name in ("SteamPath", "InstallPath"): try: roots.append(pathlib.Path(winreg.QueryValueEx(k, name)[0])) except OSError: pass except OSError: pass except ImportError: pass libs = set() for r in roots: libs.add(r) vdf = r / "steamapps" / "libraryfolders.vdf" if vdf.exists(): libs |= {pathlib.Path(p.replace("\\\\", "\\")) for p in re.findall(r'"path"\s+"([^"]+)"', vdf.read_text("utf-8", "replace"))} for lib in libs: g = lib / "steamapps" / "common" / "Enshrouded" if (g / "enshrouded.kfc").exists(): return g raise SystemExit("Enshrouded not found in the Steam libraries: pass --game ") def game_running(): out = subprocess.run(["tasklist", "/FI", "IMAGENAME eq enshrouded.exe"], capture_output=True, text=True).stdout return "enshrouded.exe" in out.lower() def fetch_tools(): tools = ROOT / "tools" tools.mkdir(exist_ok=True) for name, sha in SHA256.items(): f = tools / name if not f.exists(): print("download", REL + name) urllib.request.urlretrieve(REL + name, f) h = hashlib.sha256(f.read_bytes()).hexdigest() if sha and h != sha: f.unlink() raise SystemExit(f"{name}: SHA-256 {h} does not match the release ({sha}); deleted") return tools def emm(tools, game, *args): r = subprocess.run([str(tools / "emm.exe"), *args, "-g", str(game)], capture_output=True, text=True, encoding="utf-8", errors="replace") return r.returncode, r.stdout + r.stderr def main(): a = sys.argv[1:] game = pathlib.Path(a[a.index("--game") + 1]) if "--game" in a else find_game() print("game:", game) if game_running(): raise SystemExit("close Enshrouded first (EML cannot patch files the game holds open)") tools = fetch_tools() if "--uninstall" in a: if (game / "enshrouded.kfc.bak").exists(): code, out = emm(tools, game, "restore") print(out[-500:]) shutil.rmtree(game / "mods" / MOD, ignore_errors=True) (game / "dbghelp.dll").unlink(missing_ok=True) print("UNINSTALLED: the original game files are back; Steam 'Verify integrity' also restores them") return src = ROOT / "mod" / MOD if not (src / "mod.json").exists(): raise SystemExit(f"{src} is empty: git lfs pull (the takes are in Git LFS), or build it (README)") small = [f for f in (src / "data").glob("*.qoa") if f.stat().st_size < 1024] if small: raise SystemExit(f"{len(small)} .qoa files are LFS pointers, not audio: run 'git lfs pull' first") shutil.copyfile(tools / "dbghelp.dll", game / "dbghelp.dll") dst = game / "mods" / MOD shutil.rmtree(dst, ignore_errors=True) shutil.copytree(src, dst) print("mod copied to", dst) code, out = emm(tools, game, "run", "--export") line = [l for l in out.splitlines() if "[ru_voice]" in l] print(re.sub(r"\x1b\[[0-9;]*m", "", line[-1]) if line else out[-1500:]) if not line or " 0 failed" not in line[-1]: raise SystemExit("dry run failed: see the output above") print("INSTALLED: start the game from Steam as usual; notes are read in Russian") if __name__ == "__main__": os.environ.setdefault("PYTHONUTF8", "1") main()