DevourUnlocker/standalone/devour_unlocker.py
MiTHRAL 4af18a2f75
Some checks are pending
Build DevourUnlocker / windows-cross (push) Waiting to run
Build DevourUnlocker / linux (push) Waiting to run
Build DevourUnlocker / standalone (push) Waiting to run
v2.0.1: expand to all item types + fix pets + linux CI
- Config: add allPets (Crow/Pig/Goat/Rat/Spider/Cat/Pug), allPerks (29 inc Acceleration/Quartermaster etc), flashlights/emotes/outfits; unlockOutfits global
- Resolver: resolve HasPerk/HasPet/UpdateLocked* for Pets/Perks/Flashlights/Emotes/Outfits/Robes
- Patches: hook IsRobe/IsCharacter + HasPerk/HasOutfit + UpdateLockedPets/Perks/Flashlights/Emotes/Outfits/Robes bypass — fixes pets not unlocking
- GUI: 7 tabs (Robes/Chars/Perks/Pets/Flashlights/Emotes/Outfits), Unlock All covers all categories
- Standalone: --perks/--pets/--all now covers all categories, petOverrides etc, list shows 30 robes+9 chars+29 perks+7 pets+8 flashlights+12 emotes+8 outfits
- GUI standalone: 7 notebooks with per-category checkboxes
- BepInEx: add UpdateLocked* prefix patches + HasPerk/HasOutfit, GUI tabs for all
- CI: switch windows-latest to ubuntu-latest mingw cross (git.mithraic.cloud has only linux runner, windows jobs were stuck Waiting)
2026-08-21 21:15:58 -04:00

164 lines
7.9 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"]
PERKS = [
"Acceleration","Aftershock","Conservationist","Crawler","DuracellBunny","Evader",
"FastWorker","FieldMedic","FirstResponder","FullyCharged","Immune","Inspired",
"Keymaster","LongSighted","Patient","PowerfulInsight","Quartermaster","Regeneration",
"Repellent","Speedy","SpeedWalker","StayBack","SupplyRunner","Tamed","Teamwork",
"TeamLeader","Trapper","UnderPressure","CrawlerVent"
]
PETS = ["Crow","Pig","Goat","Rat","Spider","Cat","Pug"]
FLASHLIGHTS = ["Basic","UV","Battery","Gold","Carnival","Manor","Hospital","Infinity"]
EMOTES = ["Wave","Dance","Sit","Point","Laugh","Cry","Angry","Cheer","Clap","Taunt","Shrug","Bow"]
OUTFITS = ["Default","Formal","Casual","Winter","Summer","Anniversary","Hard","Nightmare"]
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, "unlockOutfits": 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:"); [print(f" {r}") for r in ROBES]
print("Characters:"); [print(f" {c}") for c in CHARS]
print("Perks:"); [print(f" {p}") for p in PERKS]
print("Pets:"); [print(f" {p}") for p in PETS]
print("Flashlights:"); [print(f" {f}") for f in FLASHLIGHTS]
print("Emotes:"); [print(f" {e}") for e in EMOTES]
print("Outfits:"); [print(f" {o}") for o in OUTFITS]
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 []
sel_chars = args.chars.split(",") if args.chars else []
sel_perks = args.perks.split(",") if args.perks else []
sel_pets = args.pets.split(",") if args.pets else []
if args.all:
cfg.update({"unlockRobes":True,"unlockCharacters":True,"unlockPerks":True,"unlockPets":True,"unlockFlashlights":True,"unlockEmotes":True,"unlockOutfits":True})
# clear overrides — global true covers all
for k in ["robeOverrides","characterOverrides","perkOverrides","petOverrides","flashlightOverrides","emoteOverrides","outfitOverrides"]:
cfg.pop(k,None)
save_config(args.json, cfg)
print("[*] Unlocked ALL categories (robes/chars/perks/pets/flashlights/emotes/outfits)")
return
# selective
cfg["unlockRobes"] = cfg["unlockCharacters"] = cfg["unlockPerks"] = cfg["unlockPets"] = cfg["unlockFlashlights"] = cfg["unlockEmotes"] = cfg["unlockOutfits"] = 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}
cfg["perkOverrides"] = {p: (p in sel_perks) for p in PERKS}
cfg["petOverrides"] = {p: (p in sel_pets) for p in PETS}
else:
if sel_robes: cfg.setdefault("robeOverrides", {}).update({r: True for r in sel_robes})
if sel_chars: cfg.setdefault("characterOverrides", {}).update({c: True for c in sel_chars})
if sel_perks: cfg.setdefault("perkOverrides", {}).update({p: True for p in sel_perks})
if sel_pets: cfg.setdefault("petOverrides", {}).update({p: True for p in sel_pets})
# if no args, just ensure globals true
if not any([sel_robes, sel_chars, sel_perks, sel_pets]):
pass
if args.lock:
for k in ["unlockRobes","unlockCharacters","unlockPerks","unlockPets","unlockFlashlights","unlockEmotes","unlockOutfits"]:
cfg[k]=False
cfg["robeOverrides"] = {r: False for r in ROBES}
cfg["characterOverrides"] = {c: False for c in CHARS}
cfg["perkOverrides"] = {p: False for p in PERKS}
cfg["petOverrides"] = {p: False for p in PETS}
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("--perks", help="comma list e.g. Speedy,Patient")
su.add_argument("--pets", help="comma list e.g. Crow,Pig")
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()