v2.0.4: fix standalone via collectables.json + pet hook + winhttp forwarding
Some checks failed
Build DevourUnlocker / windows-cross (push) Has been cancelled
Build DevourUnlocker / linux (push) Has been cancelled
Build DevourUnlocker / standalone (push) Has been cancelled

- winhttp: runtime forwarding for all WinHttp* (fixes Wine crash on WinHttpGetIEProxyConfigForCurrentUser, was forwarding to winhttp_real.dll which doesn't exist)
- resolver: wait for IL2CPP domain + enumerate all images/classes for IsRobe/UpdateLocked*, dumpDiagnostics now shows FOUND for SurvivalLobbyController etc
- patches: handle MH_ERROR_ALREADY_INITIALIZED, background retry for 60s for UpdateLockedPets/WaitForPurchasedPetValidation (fixes pets not unlocking when lobby not yet loaded)
- standalone: unlock --all now patches Steam Cloud collectables.json for all 3 users (BarbedWires/Feathers/Tickets etc to superset) and updates remotecache.vdf sha/size/time so Steam doesn't overwrite; also auto-deploys winhttp.dll via injector
- diagnose: checks winhttp.dll, DevourUnlocker.log, WINEDLLOVERRIDES, collectables.json
- launch.sh: sets WINEDLLOVERRIDES and launches via Proton
This commit is contained in:
MiTHRAL 2026-08-22 19:19:32 -04:00
parent 77f86c00ac
commit f1a75f8f71
4 changed files with 232 additions and 20 deletions

View file

@ -3,6 +3,8 @@
#include <cstring>
#include <thread>
#include <chrono>
#include <fstream>
#include <filesystem>
#ifdef _WIN32
#include <windows.h>
@ -69,8 +71,10 @@ std::vector<void*> Il2CppResolver::scanPattern(const char* pattern, const char*
}
ResolvedMethod Il2CppResolver::resolve(const char* klassName, const char* methodName) {
// Wait for IL2CPP domain to be ready — early MainThread may call before Unity inits IL2CPP
if (!waitForDomain(15000)) {
// Domain wait is now handled by installPatches once, not per-resolve to avoid 6*15s stalls
// If domain not ready, fallback to string xref quickly
auto domain_get_test = reinterpret_cast<void*(*)()>(getProc("il2cpp_domain_get"));
if (domain_get_test && !domain_get_test()) {
return resolveByStringXref(methodName);
}
auto il2cpp_domain_get = reinterpret_cast<void*(*)()>(getProc("il2cpp_domain_get"));
@ -95,12 +99,16 @@ ResolvedMethod Il2CppResolver::resolve(const char* klassName, const char* method
}
if (!assemblies || nAssemblies==0) return resolveByStringXref(methodName);
const char* candidates[] = {klassName, "NolanRankController", "LobbyController", "InventoryController", "GameController", nullptr};
const char* candidates[] = {klassName, "NolanRankController", "LobbyController", "InventoryController", "GameController", "SurvivalLobbyController", "NolanRankController", nullptr};
for (size_t ai = 0; ai < nAssemblies; ++ai) {
void* image = assembly_get_image((void*)assemblies[ai]);
if (!image) continue;
for (int ci = 0; candidates[ci]; ++ci) {
void* klass = klass_from_name(image, "", candidates[ci]);
if (!klass) {
// Try with no namespace but also try with "Devour" namespace?
klass = klass_from_name(image, "Devour", candidates[ci]);
}
if (!klass) continue;
void* method = class_get_method(klass, methodName);
if (method) {
@ -180,12 +188,110 @@ ResolvedMethod Il2CppResolver::isCharacterUnlocked() { return resolve("NolanRank
ResolvedMethod Il2CppResolver::hasOutfit() { return resolve("NolanRankController", "HasOutfit"); }
ResolvedMethod Il2CppResolver::hasPerk() { return resolve("NolanRankController", "HasPerk"); }
ResolvedMethod Il2CppResolver::hasPet() { return resolve("NolanRankController", "HasPet"); }
ResolvedMethod Il2CppResolver::updateLockedPets() { return resolve("LobbyController", "UpdateLockedPets"); }
ResolvedMethod Il2CppResolver::updateLockedPerks() { return resolve("LobbyController", "UpdateLockedPerks"); }
ResolvedMethod Il2CppResolver::updateLockedFlashlights() { return resolve("LobbyController", "UpdateLockedFlashlights"); }
ResolvedMethod Il2CppResolver::updateLockedEmotes() { return resolve("LobbyController", "UpdateLockedEmotes"); }
ResolvedMethod Il2CppResolver::updateLockedOutfits() { return resolve("LobbyController", "UpdateLockedOutfits"); }
ResolvedMethod Il2CppResolver::updateLockedRobes() { return resolve("LobbyController", "UpdateLockedRobes"); }
ResolvedMethod Il2CppResolver::updateLockedPets() { return resolve("SurvivalLobbyController", "UpdateLockedPets"); }
ResolvedMethod Il2CppResolver::updateLockedPerks() { return resolve("SurvivalLobbyController", "UpdateLockedPerks"); }
ResolvedMethod Il2CppResolver::updateLockedFlashlights() { return resolve("SurvivalLobbyController", "UpdateLockedFlashlights"); }
ResolvedMethod Il2CppResolver::updateLockedEmotes() { return resolve("SurvivalLobbyController", "UpdateLockedEmotes"); }
ResolvedMethod Il2CppResolver::updateLockedOutfits() { return resolve("SurvivalLobbyController", "UpdateLockedOutfits"); }
ResolvedMethod Il2CppResolver::updateLockedRobes() { return resolve("SurvivalLobbyController", "UpdateLockedRobes"); }
ResolvedMethod Il2CppResolver::waitForPurchasedPet() { return resolve("SurvivalLobbyController", "WaitForPurchasedPetValidation"); }
void Il2CppResolver::dumpDiagnostics(){
#ifdef _WIN32
// Also log via OutputDebugString for Wine debug
OutputDebugStringA("[Diag] dumpDiagnostics enter\n");
try{
char exe[MAX_PATH]; GetModuleFileNameA(nullptr, exe, MAX_PATH);
char* slash = strrchr(exe, '\\'); if(!slash) slash = strrchr(exe, '/');
std::string logPath = slash ? std::string(exe, slash - exe) + "\\DevourUnlocker.log" : "DevourUnlocker.log";
std::ofstream f(logPath, std::ios::app);
f << "[Diag] dumpDiagnostics ENTER\n"; f.flush();
auto il2cpp_domain_get = reinterpret_cast<void*(*)()>(getProc("il2cpp_domain_get"));
auto domain_get_assemblies = reinterpret_cast<const void**(*)(void*, size_t*)>(getProc("il2cpp_domain_get_assemblies"));
auto assembly_get_image = reinterpret_cast<void*(*)(void*)>(getProc("il2cpp_assembly_get_image"));
auto image_get_class_count = reinterpret_cast<int(*)(void*)>(getProc("il2cpp_image_get_class_count"));
auto image_get_class = reinterpret_cast<void*(*)(void*, int)>(getProc("il2cpp_image_get_class"));
auto class_get_name = reinterpret_cast<const char*(*)(void*)>(getProc("il2cpp_class_get_name"));
auto class_get_namespace = reinterpret_cast<const char*(*)(void*)>(getProc("il2cpp_class_get_namespace"));
auto class_get_method_count = reinterpret_cast<int(*)(void*)>(getProc("il2cpp_class_num_methods"));
auto class_get_methods = reinterpret_cast<void**(*)(void*, void**)>(getProc("il2cpp_class_get_methods"));
auto method_get_name = reinterpret_cast<const char*(*)(void*)>(getProc("il2cpp_method_get_name"));
if(!il2cpp_domain_get || !domain_get_assemblies || !assembly_get_image || !image_get_class_count || !image_get_class) {
f << "[Diag] missing IL2CPP exports for dump\n"; return;
}
void* domain = il2cpp_domain_get();
if(!domain){ f << "[Diag] domain null\n"; return; }
size_t nAssemblies=0; const void** assemblies = domain_get_assemblies(domain, &nAssemblies);
f << "[Diag] assemblies=" << nAssemblies << "\n";
for(size_t ai=0; ai<nAssemblies && ai<32; ++ai){
void* image = assembly_get_image((void*)assemblies[ai]);
if(!image) continue;
auto image_get_name = reinterpret_cast<const char*(*)(void*)>(getProc("il2cpp_image_get_name"));
const char* iname = image_get_name ? image_get_name(image) : "?";
int cnt = image_get_class_count(image);
f << "[Diag] image " << iname << " classes=" << cnt << " ptr=" << image << "\n";
// Log first 10 class names of this image as sample
for(int ci=0; ci<cnt && ci<10; ++ci){
void* klass = image_get_class(image, ci);
if(!klass) continue;
const char* kname = class_get_name ? class_get_name(klass) : "?";
const char* ns = class_get_namespace ? class_get_namespace(klass) : "";
f << " sample class " << ns << "." << kname << "\n";
}
// Now search for UpdateLocked etc in all classes of this image — also dump Nolan/Survival specifically
for(int ci=0; ci<cnt && ci<2000; ++ci){
void* klass = image_get_class(image, ci);
if(!klass) continue;
const char* kname = class_get_name ? class_get_name(klass) : "?";
std::string kn=kname?kname:"";
bool isTargetKlass = kn=="NolanRankController" || kn=="SurvivalLobbyController" || kn=="LobbyController";
auto class_get_method = reinterpret_cast<void*(*)(void*, const char*)>(getProc("il2cpp_class_get_method_from_name"));
if(isTargetKlass && class_get_method){
f << " DIAG target klass " << kname << " in " << iname << "\n";
const char* targets[]={"UpdateLockedPets","UpdateLockedPerks","IsRobeUnlocked","IsCharacterUnlocked","HasPerk","HasOutfit","UpdateLockedRobes",nullptr};
for(int ti=0; targets[ti]; ++ti){
void* m = class_get_method(klass, targets[ti]);
f << " try " << targets[ti] << " -> " << m;
if(m){
void* ptr = *reinterpret_cast<void**>(reinterpret_cast<uint8_t*>(m) + 0x18);
f << " ptr=" << ptr;
auto method_get_name2 = reinterpret_cast<const char*(*)(void*)>(getProc("il2cpp_method_get_name"));
if(method_get_name2) f << " name=" << method_get_name2(m);
}
f << "\n";
}
// Dump all methods of this klass
auto class_get_methods2 = reinterpret_cast<const void**(*)(void*, void**)>(getProc("il2cpp_class_get_methods"));
auto method_get_name2 = reinterpret_cast<const char*(*)(void*)>(getProc("il2cpp_method_get_name"));
if(class_get_methods2 && method_get_name2){
void* iter2=nullptr;
int mc2=0;
f << " all methods of " << kname << ":\n";
while(true){
const void** methods = (const void**)class_get_methods2(klass, &iter2);
if(!methods || !*methods) break;
const char* mn = method_get_name2((void*)*methods);
f << " " << (mn?mn:"<null>") << "\n";
if(++mc2>100) break;
}
}
}
if(class_get_method){
const char* targets[]={"UpdateLockedPets","UpdateLockedPerks","IsRobeUnlocked","IsCharacterUnlocked","HasPerk","HasOutfit",nullptr};
for(int ti=0; targets[ti]; ++ti){
void* m = class_get_method(klass, targets[ti]);
if(m) {
void* ptr = *reinterpret_cast<void**>(reinterpret_cast<uint8_t*>(m) + 0x18);
f << " FOUND " << kname << "." << targets[ti] << " @ " << m << " -> " << ptr << " (image " << iname << ")\n";
}
}
}
}
}
f << "[Diag] dump done\n";
}catch(...){}
#endif
}
#else // Linux stubs
@ -207,6 +313,7 @@ ResolvedMethod Il2CppResolver::updateLockedFlashlights() { return {"UpdateLocked
ResolvedMethod Il2CppResolver::updateLockedEmotes() { return {"UpdateLockedEmotes", nullptr}; }
ResolvedMethod Il2CppResolver::updateLockedOutfits() { return {"UpdateLockedOutfits", nullptr}; }
ResolvedMethod Il2CppResolver::updateLockedRobes() { return {"UpdateLockedRobes", nullptr}; }
ResolvedMethod Il2CppResolver::waitForPurchasedPet() { return {"WaitForPurchasedPetValidation", nullptr}; }
#endif

View file

@ -39,6 +39,8 @@ public:
static ResolvedMethod updateLockedEmotes();
static ResolvedMethod updateLockedOutfits();
static ResolvedMethod updateLockedRobes();
static ResolvedMethod waitForPurchasedPet();
static void dumpDiagnostics();
static uintptr_t gameAssemblyBase();
static size_t gameAssemblySize();
};

View file

@ -97,12 +97,15 @@ static bool __fastcall hHasOutfit(void* thiz, void* outfitStr, void* method) {
static void __fastcall hUpdateLockedPets(void* thiz, void* method) {
auto& cfg = Config::get();
if (!cfg.unlockPets) {
// fall through to original — we need original ptr; use hookUpdateLocked helper instead
return;
}
std::cout << "[DevourUnlocker] UpdateLockedPets bypassed (pets unlocked)\n";
// Don't call original — it would set locked visuals. Instead we directly ensure inventory shows unlocked.
// Original is void, so just return. The UI will consider all pets unlocked because HasPet/GetPet hooks would return true.
logPatch("[DevourUnlocker] UpdateLockedPets bypassed");
}
static void __fastcall hWaitForPurchasedPet(void* thiz, void* method) {
if (!Config::get().unlockPets) return;
std::cout << "[DevourUnlocker] WaitForPurchasedPetValidation bypassed\n";
logPatch("[DevourUnlocker] WaitForPurchasedPetValidation bypassed");
}
static void __fastcall hUpdateLockedPerks(void* thiz, void* method) {
if (!Config::get().unlockPerks) return;
@ -159,6 +162,17 @@ bool installPatches() {
return false;
}
logPatch("[DevourUnlocker] GameAssembly found");
// Wait for IL2CPP domain to be ready (Unity inits it a few seconds after GameAssembly load)
{
auto domain_get = reinterpret_cast<void*(*)()>(Il2CppResolver::getProc("il2cpp_domain_get"));
if (domain_get) {
for(int i=0;i<100;i++){
if(domain_get()){ logPatch("[DevourUnlocker] IL2CPP domain ready"); break; }
Sleep(100);
}
if(!domain_get()) logPatch("[DevourUnlocker] IL2CPP domain still null after wait");
}
}
auto robe = Il2CppResolver::isRobeUnlocked();
auto ch = Il2CppResolver::isCharacterUnlocked();
auto hasPerk = Il2CppResolver::hasPerk();
@ -169,22 +183,39 @@ bool installPatches() {
auto updEmotes = Il2CppResolver::updateLockedEmotes();
auto updOutfits = Il2CppResolver::updateLockedOutfits();
auto updRobes = Il2CppResolver::updateLockedRobes();
auto waitPet = Il2CppResolver::waitForPurchasedPet();
std::cout << "[DevourUnlocker] Resolved IsRobeUnlocked=" << robe.address
<< " IsCharacterUnlocked=" << ch.address
<< " HasPerk=" << hasPerk.address << " HasOutfit=" << hasOutfit.address << "\n";
std::cout << "[DevourUnlocker] UpdateLocked: Pets=" << updPets.address << " Perks=" << updPerks.address
<< " Flash=" << updFlash.address << " Emotes=" << updEmotes.address
<< " Outfits=" << updOutfits.address << " Robes=" << updRobes.address << "\n";
<< " Outfits=" << updOutfits.address << " Robes=" << updRobes.address << " WaitPet=" << waitPet.address << "\n";
{
char buf[1024];
snprintf(buf, sizeof(buf), "[DevourUnlocker] Resolved IsRobe=%p IsChar=%p HasPerk=%p HasOutfit=%p", robe.address, ch.address, hasPerk.address, hasOutfit.address);
logPatch(buf);
snprintf(buf, sizeof(buf), "[DevourUnlocker] UpdateLocked Pets=%p Perks=%p Flash=%p Emotes=%p Outfits=%p Robes=%p WaitPet=%p", updPets.address, updPerks.address, updFlash.address, updEmotes.address, updOutfits.address, updRobes.address, waitPet.address);
logPatch(buf);
}
bool any = false;
// Diagnostic dump if UpdateLocked* not found earlier
if (!updPets.valid() || !updPerks.valid() || !robe.valid()) {
logPatch("[DevourUnlocker] UpdateLocked/IsRobe not found — dumping IL2CPP diagnostics");
OutputDebugStringA("[DevourUnlocker] dumping diagnostics\n");
Il2CppResolver::dumpDiagnostics();
logPatch("[DevourUnlocker] dumpDiagnostics returned");
}
if (robe.valid()) {
if (MH_CreateHook(robe.address, (void*)hIsRobe, (void**)&oIsRobe) == MH_OK) {
MH_EnableHook(robe.address);
std::cout << "[DevourUnlocker] Hooked IsRobeUnlocked\n";
logPatch("[DevourUnlocker] Hooked IsRobeUnlocked");
any = true;
} else std::cerr << "[DevourUnlocker] Hook IsRobe failed\n";
} else {
std::cerr << "[DevourUnlocker] IsRobeUnlocked not found — will retry on next init or use standalone\n";
logPatch("[DevourUnlocker] IsRobeUnlocked not found");
}
if (ch.valid()) {
if (MH_CreateHook(ch.address, (void*)hIsChar, (void**)&oIsChar) == MH_OK) {
@ -213,31 +244,44 @@ bool installPatches() {
any |= hookUpdateLocked(updEmotes.address, "UpdateLockedEmotes", Config::get().unlockEmotes, hUpdateLockedEmotes);
any |= hookUpdateLocked(updOutfits.address, "UpdateLockedOutfits", Config::get().unlockOutfits, hUpdateLockedOutfits);
any |= hookUpdateLocked(updRobes.address, "UpdateLockedRobes", Config::get().unlockRobes, hUpdateLockedRobes);
// If UpdateLocked* not found, it may be because IL2CPP not yet ready — retry in background (fixes pets not unlocking)
bool needRetry = !updPets.valid() || !updPerks.valid() || !updFlash.valid() || !updEmotes.valid() || !updOutfits.valid() || !updRobes.valid();
any |= hookUpdateLocked(waitPet.address, "WaitForPurchasedPetValidation", Config::get().unlockPets, hWaitForPurchasedPet);
// If any hooks not found, IL2CPP may not be ready until lobby — retry for 60s
bool needRetry = !updPets.valid() || !updPerks.valid() || !updFlash.valid() || !updEmotes.valid() || !updOutfits.valid() || !updRobes.valid() || !robe.valid() || !ch.valid() || !waitPet.valid();
if (needRetry) {
logPatch("[DevourUnlocker] Some UpdateLocked* not found — scheduling retry in 3s (IL2CPP may not be ready)");
logPatch("[DevourUnlocker] Some hooks not found — scheduling retry for 60s (IL2CPP may not be ready until lobby)");
CreateThread(nullptr, 0, [](LPVOID)->DWORD{
for(int attempt=0; attempt<5; ++attempt){
for(int attempt=0; attempt<20; ++attempt){
Sleep(3000);
auto r = Il2CppResolver::isRobeUnlocked();
auto c = Il2CppResolver::isCharacterUnlocked();
auto hp = Il2CppResolver::hasPerk();
auto ho = Il2CppResolver::hasOutfit();
auto p1 = Il2CppResolver::updateLockedPets();
auto p2 = Il2CppResolver::updateLockedPerks();
auto p3 = Il2CppResolver::updateLockedFlashlights();
auto p4 = Il2CppResolver::updateLockedEmotes();
auto p5 = Il2CppResolver::updateLockedOutfits();
auto p6 = Il2CppResolver::updateLockedRobes();
auto pw = Il2CppResolver::waitForPurchasedPet();
bool any2=false;
if(r.valid()){ if(MH_CreateHook(r.address,(void*)hIsRobe,(void**)&oIsRobe)==MH_OK){ MH_EnableHook(r.address); any2=true; logPatch("[DevourUnlocker] Retry hooked IsRobeUnlocked"); } }
if(c.valid()){ if(MH_CreateHook(c.address,(void*)hIsChar,(void**)&oIsChar)==MH_OK){ MH_EnableHook(c.address); any2=true; logPatch("[DevourUnlocker] Retry hooked IsCharacterUnlocked"); } }
if(p1.valid()) any2 |= hookUpdateLocked(p1.address, "UpdateLockedPets[retry]", Config::get().unlockPets, hUpdateLockedPets);
if(p2.valid()) any2 |= hookUpdateLocked(p2.address, "UpdateLockedPerks[retry]", Config::get().unlockPerks, hUpdateLockedPerks);
if(p3.valid()) any2 |= hookUpdateLocked(p3.address, "UpdateLockedFlashlights[retry]", Config::get().unlockFlashlights, hUpdateLockedFlashlights);
if(p4.valid()) any2 |= hookUpdateLocked(p4.address, "UpdateLockedEmotes[retry]", Config::get().unlockEmotes, hUpdateLockedEmotes);
if(p5.valid()) any2 |= hookUpdateLocked(p5.address, "UpdateLockedOutfits[retry]", Config::get().unlockOutfits, hUpdateLockedOutfits);
if(p6.valid()) any2 |= hookUpdateLocked(p6.address, "UpdateLockedRobes[retry]", Config::get().unlockRobes, hUpdateLockedRobes);
if(any2){ logPatch("[DevourUnlocker] Retry hooked some UpdateLocked"); break; }
if(pw.valid()) any2 |= hookUpdateLocked(pw.address, "WaitForPurchasedPetValidation[retry]", Config::get().unlockPets, hWaitForPurchasedPet);
if(any2){ logPatch("[DevourUnlocker] Retry hooked some — dumping diagnostics"); Il2CppResolver::dumpDiagnostics(); break; }
if(attempt==5 || attempt==10 || attempt==19) {
char buf[128]; snprintf(buf,sizeof(buf),"[DevourUnlocker] Retry attempt %d still not found",attempt);
logPatch(buf);
if(attempt==10) Il2CppResolver::dumpDiagnostics();
}
}
return 0;
}, nullptr, 0, nullptr);
// Also try IsRobe/HasPerk etc retry
}
g_installed = any;
logPatch(any ? "[DevourUnlocker] Patches installed (some may retry)" : "[DevourUnlocker] No patches installed");

View file

@ -92,6 +92,64 @@ def cmd_status(args):
for f in p.glob("*"):
print(" ", f.name)
def _patch_collectables_all():
"""Directly patch Steam Cloud collectables.json for every local user — makes robes unlock without needing DLL hook."""
# Known full sets from observing global-metadata + user files (union of all seen)
full = {
"Collectables": ["T","H","B","Y","A","S","M","G","D","X","A","B","Y"], # will dedupe
"Patches": ["MX","MN","MF","ST","SA","SP","SQ","SG","SF"],
"CherryBlossom": ["SA","SV","SK","ST","SP","SQ","SG","SF","CX","CU"], # placeholder — game will treat any superset as unlocked (AllCherryBlossomFound checks count)
"Horseshoes": ["ST","SA","SP","SQ","SG","SF","SA","SV","SK"],
"Pumpkins": ["SA","SV","SK"],
"Presents": ["SA","SV","SK"],
"BarbedWires": ["SA","SV","SK","ST","SA","SP","SQ","SG","SF","SV","SK"],
"Feathers": ["MAX","MAT","ST","SA","SP","SQ","SG","SF"],
"Tickets": ["CX","CU","CF","CD","CA","CM","CH","ST","SA"],
}
# Also ensure each is deduped and full for unlock checks (All*Found checks count == expected)
# For safety, set each to the union of observed + 20 dummy entries so count check passes
import glob, hashlib
for base in pathlib.Path.home().glob(".local/share/Steam/userdata/*/1274570/remote/collectables.json"):
try:
j=json.loads(base.read_text())
for k,v in full.items():
j[k] = sorted(set(v + j.get(k,[])))
if k in ["BarbedWires","Feathers","Tickets","CherryBlossom","Horseshoes"]:
if len(j[k]) < 6:
j[k] = sorted(set(j[k] + ["SA","SV","SK","ST","SP","SQ","SG","SF","MAX","MAT","CX","CU","CF","CD","CA","CM","CH"]))
txt=json.dumps(j)
base.write_text(txt)
# Update remotecache.vdf so Steam doesn't overwrite with cloud (must update size/sha/time)
rc = base.parent.parent / "remotecache.vdf"
if rc.exists():
try:
t=rc.read_text()
import re, time, hashlib
sha=hashlib.sha1(txt.encode()).hexdigest()
now=str(int(time.time()))
# Replace the whole collectables.json block via simple string replace for size/sha/time
# Find the block
old_size=re.search(r'"collectables\.json"\s*\{\s*"root"\s*"[^"]*"\s*"size"\s*"(\d+)"', t)
if old_size:
t=t.replace(f'"size" "{old_size.group(1)}"', f'"size" "{len(txt)}"', 1)
# sha
old_sha=re.search(r'"collectables\.json".*?"sha"\s*"([^"]+)"', t, re.DOTALL)
if old_sha:
t=t.replace(f'"sha" "{old_sha.group(1)}"', f'"sha" "{sha}"', 1)
# localtime/time
t=re.sub(r'("collectables\.json".*?"localtime"\s*")\d+(")', rf'\g<1>{now}\2', t, count=1, flags=re.DOTALL)
t=re.sub(r'("collectables\.json".*?"time"\s*")\d+(")', rf'\g<1>{now}\2', t, count=1, flags=re.DOTALL)
rc.write_text(t)
except Exception as e:
print(f"[!] remotecache patch failed for {rc}: {e}")
print(f"[+] Patched {base} -> { {k:len(v) for k,v in j.items()} } (sha {hashlib.sha1(txt.encode()).hexdigest()[:8]})")
except Exception as e:
import traceback; traceback.print_exc()
print(f"[!] Failed to patch {base}: {e}")
# Also patch local PlayerPrefs user.reg BarbedWires etc via hex
# For Proton, user.reg approach is more complex, so we also ensure DevourUnlocker.json is set
return True
def cmd_unlock(args):
cfg = load_config(args.json)
sel_robes = args.robes.split(",") if args.robes else []
@ -104,11 +162,12 @@ def cmd_unlock(args):
cfg.pop(k,None)
save_config(args.json, cfg)
print("[*] Unlocked ALL categories (robes/chars/perks/pets/flashlights/emotes/outfits)")
# Auto-deploy mod so standalone actually works (was JSON-only before → "does nothing")
_patch_collectables_all()
try:
import injector
injector.ensure_mod_deployed(verbose=True)
print("[*] Mod DLLs ensured — launch with WINEDLLOVERRIDES=\"winhttp=n,b\" %command% or run: python standalone/injector.py")
print("[*] Also patched collectables.json directly — standalone should now work even without DLL after Steam Cloud sync")
except Exception as e:
print(f"[!] Could not auto-deploy mod: {e}")
return