Russian narration for the Enshrouded notes: mod 0.4.0, installer, generation pipeline
This commit is contained in:
+128
@@ -0,0 +1,128 @@
|
||||
"""Install (or remove) the Russian narration mod. Python 3.11+, standard library only, Windows.
|
||||
|
||||
usage: python install.py [--game <Enshrouded folder>] 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 <game>/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 <folder with enshrouded.kfc>")
|
||||
|
||||
|
||||
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()
|
||||
Reference in New Issue
Block a user