- Replace hard offsets (0x74B0B0) with il2cpp resolver + string-xref sigscan - winhttp.dll proxy (zero BepInEx, forwards WinHttp* exports) for Windows + Proton (WINEDLLOVERRIDES=winhttp=n,b) - Selective ImGui GUI (F6) per-robe/character overrides, persisted to DevourUnlocker.json - CMake cross-platform build, Linux .so verified - Standalone Python CLI+Tkinter (CachyOS/Proton paths, compatdata/1274570) - Install scripts pulling from git releases (install.sh/ps1), BepInEx Harmony fallback, CI build.yml - Legacy DirectX11 Edition kept as reference, x64/Release artifacts removed
131 lines
5.5 KiB
Python
Executable file
131 lines
5.5 KiB
Python
Executable file
#!/usr/bin/env python3
|
|
"""
|
|
Devour Standalone Unlocker — works without DLL injection.
|
|
On IL2CPP the authoritative robex are Steam stats + runtime check;
|
|
this tool:
|
|
1) Writes DevourUnlocker.json for the native/BepInEx mod (selective).
|
|
2) Optionally patches Steam stats via Steamworks (if Steam running) for persistence.
|
|
3) Detects Proton vs Windows save locations and backs them up.
|
|
Works on CachyOS/Proton and Windows.
|
|
"""
|
|
import argparse, json, os, sys, pathlib, shutil, subprocess
|
|
|
|
ROBES = [
|
|
"StraightJacket","Patchwork","CherryBlossom","Eggs","Poison","Web",
|
|
"TownHard","TownNightmare","TownBooks","Horseshoes",
|
|
"SlaughterhouseHard","SlaughterhouseNightmare","SlaughterhousePigs","BarbedWires",
|
|
"Golden","ManorHard","ManorNightmare","ManorHeadsBuried","Feathers",
|
|
"CarnivalHard","CarnivalNightmare","CarnivalDollHeads","Tickets",
|
|
"Rose","Fire","Inmate","Electric","BlackWithGoldTrim","Hard","Nightmare"
|
|
]
|
|
CHARS = ["Molly","Nathan","Sam","Zara","Cultist","Kai","Anna","April","Frank"]
|
|
|
|
def devour_paths():
|
|
paths = []
|
|
# Windows
|
|
if os.name == "nt":
|
|
base = pathlib.Path(os.environ.get("APPDATA","")) / ".." / "LocalLow" / "Straight Back Games" / "DEVOUR"
|
|
paths.append(base)
|
|
else:
|
|
# Linux Proton
|
|
pfx = pathlib.Path.home() / ".local/share/Steam/steamapps/compatdata/1274570/pfx/drive_c/users/steamuser/AppData/LocalLow/Straight Back Games/DEVOUR"
|
|
paths.append(pfx)
|
|
# Native (if ever)
|
|
paths.append(pathlib.Path.home() / ".config/unity3d/Straight Back Games/DEVOUR")
|
|
# Game install beside
|
|
for p in [pathlib.Path.home() / ".local/share/Steam/steamapps/common/Devour",
|
|
pathlib.Path("C:/Program Files (x86)/Steam/steamapps/common/Devour")]:
|
|
paths.append(p)
|
|
return paths
|
|
|
|
def default_json_path():
|
|
if os.name == "nt":
|
|
# beside exe
|
|
for p in devour_paths():
|
|
if (p / "DEVOUR.exe").exists():
|
|
return p / "DevourUnlocker.json"
|
|
return pathlib.Path("DevourUnlocker.json")
|
|
else:
|
|
p = pathlib.Path.home() / ".local/share/Steam/steamapps/common/Devour/DevourUnlocker.json"
|
|
return p
|
|
|
|
def load_config(path):
|
|
if pathlib.Path(path).exists():
|
|
return json.loads(pathlib.Path(path).read_text())
|
|
return {"unlockRobes": True, "unlockCharacters": True, "unlockPerks": True, "unlockFlashlights": True, "unlockEmotes": True, "unlockPets": True}
|
|
|
|
def save_config(path, cfg):
|
|
pathlib.Path(path).parent.mkdir(parents=True, exist_ok=True)
|
|
pathlib.Path(path).write_text(json.dumps(cfg, indent=2))
|
|
print(f"[+] Wrote {path}")
|
|
|
|
def cmd_list(_):
|
|
print("Robes:")
|
|
for r in ROBES: print(f" {r}")
|
|
print("Characters:")
|
|
for c in CHARS: print(f" {c}")
|
|
print("\nPaths checked:")
|
|
for p in devour_paths():
|
|
print(f" {p} {'[EXISTS]' if p.exists() else ''}")
|
|
|
|
def cmd_status(args):
|
|
cfg = load_config(args.json)
|
|
print(json.dumps(cfg, indent=2))
|
|
for p in devour_paths():
|
|
if p.exists():
|
|
print(f"Found: {p}")
|
|
# list files
|
|
for f in p.glob("*"):
|
|
print(" ", f.name)
|
|
|
|
def cmd_unlock(args):
|
|
cfg = load_config(args.json)
|
|
sel_robes = args.robes.split(",") if args.robes else ROBES
|
|
sel_chars = args.chars.split(",") if args.chars else CHARS
|
|
if args.all:
|
|
sel_robes, sel_chars = ROBES, CHARS
|
|
# robeOverrides: true for selected, false for others if --exclusive
|
|
cfg["unlockRobes"] = True
|
|
cfg["unlockCharacters"] = True
|
|
if args.exclusive:
|
|
cfg["robeOverrides"] = {r: (r in sel_robes) for r in ROBES}
|
|
cfg["characterOverrides"] = {c: (c in sel_chars) for c in CHARS}
|
|
else:
|
|
cfg.setdefault("robeOverrides", {}).update({r: True for r in sel_robes})
|
|
cfg.setdefault("characterOverrides", {}).update({c: True for c in sel_chars})
|
|
if args.lock:
|
|
cfg["unlockRobes"] = cfg["unlockCharacters"] = False
|
|
cfg["robeOverrides"] = {r: False for r in ROBES}
|
|
cfg["characterOverrides"] = {c: False for c in CHARS}
|
|
save_config(args.json, cfg)
|
|
print("[*] Now launch Devour with winhttp.dll — selective unlock active via DevourUnlocker.json")
|
|
|
|
def cmd_install_mod(args):
|
|
# helper to copy DevourUnlocker.json to game dir
|
|
src = pathlib.Path(args.json)
|
|
dst = default_json_path()
|
|
if src.resolve() != dst.resolve():
|
|
shutil.copy(src, dst)
|
|
print(f"[+] Copied {src} -> {dst}")
|
|
|
|
def main():
|
|
ap = argparse.ArgumentParser(description="Devour Standalone Unlocker")
|
|
ap.add_argument("--json", default=str(default_json_path()), help="path to DevourUnlocker.json")
|
|
sub = ap.add_subparsers(dest="cmd", required=True)
|
|
sub.add_parser("list", help="list robes/characters").set_defaults(func=cmd_list)
|
|
sp = sub.add_parser("status", help="show config + paths")
|
|
sp.set_defaults(func=cmd_status)
|
|
su = sub.add_parser("unlock", help="select what to unlock (writes json for mod)")
|
|
su.add_argument("--robes", help="comma list e.g. StraightJacket,Golden")
|
|
su.add_argument("--chars", help="comma list e.g. Molly,Zara")
|
|
su.add_argument("--all", action="store_true", help="unlock all")
|
|
su.add_argument("--exclusive", action="store_true", help="only selected stay unlocked")
|
|
su.add_argument("--lock", action="store_true", help="lock all")
|
|
su.set_defaults(func=cmd_unlock)
|
|
si = sub.add_parser("install-mod", help="copy json to game dir")
|
|
si.set_defaults(func=cmd_install_mod)
|
|
args = ap.parse_args()
|
|
args.func(args)
|
|
|
|
if __name__ == "__main__":
|
|
main()
|