Compare commits

..

No commits in common. "main" and "v2.0.9" have entirely different histories.
main ... v2.0.9

6 changed files with 68 additions and 457 deletions

View file

@ -138,26 +138,65 @@ python3 "$(dirname "$0")/../standalone/devour_unlocker.py" unlock --all 2>&1 | t
echo "[*] Installed to $GAME_DIR" echo "[*] Installed to $GAME_DIR"
# Auto-set Steam launch options for Proton silently (so user never needs manual WINEDLLOVERRIDES) # Auto-set Steam launch options for Proton silently (so user never needs manual WINEDLLOVERRIDES)
# Uses scripts/vdf_patch.py — bash-escaped heredoc python caused \\\" corruption before.
if [[ "$OS" == "linux" ]]; then if [[ "$OS" == "linux" ]]; then
if pgrep -x steam >/dev/null 2>&1; then patched_any=0
echo "[!] Steam is RUNNING — it will overwrite localconfig.vdf on exit and drop our LaunchOptions." for lc in "$HOME/.local/share/Steam/userdata/"*/config/localconfig.vdf; do
echo " Close Steam first: steam -shutdown then re-run install.sh" if [[ -f "$lc" ]] && grep -q '"1274570"' "$lc"; then
fi cp "$lc" "$lc.bak.$(date +%s)" 2>/dev/null || true
# PRIMARY (Steam-proof): per-prefix Wine registry override — Steam never touches user.reg python3 -c "
if [[ -f "$(dirname "$0")/prefix_override.py" ]]; then import pathlib, re, sys
python3 "$(dirname "$0")/prefix_override.py" 2>&1 | head -n 10 lc=sys.argv[1]
else LAUNCH='WINEDLLOVERRIDES=\\\\\"version=n,b\\\\\" %command%'
echo "[!] prefix_override.py missing — registry override not applied" p=pathlib.Path(lc)
fi txt=p.read_text(errors='ignore')
# SECONDARY: Steam LaunchOptions (harmless if Steam drops it later) lines=txt.splitlines(True)
if [[ -f "$(dirname "$0")/vdf_patch.py" ]]; then patched=0
python3 "$(dirname "$0")/vdf_patch.py" 2>&1 | head -n 20 && patched_any=1 i=0
else while i < len(lines)-1:
echo "[!] vdf_patch.py missing — cannot auto-set Launch Options" if lines[i].strip()=='\"1274570\"' and lines[i].startswith('\t'*5) and lines[i+1].strip()=='{' and lines[i+1].startswith('\t'*5):
fi start=i+1; depth=1; j=i+2; found=None; found_idx=None; end=None
while j < len(lines):
s=lines[j].strip()
if s=='{': depth+=1
elif s=='}':
depth-=1
if depth==0: end=j; break
if s.startswith('\"LaunchOptions\"') and lines[j].startswith('\t'*6):
found=lines[j]; found_idx=j
j+=1
if end is None: i=j+1; continue
if found_idx is not None:
if 'version=n,b' in found and 'WINEDLLOVERRIDES' in found:
if '\\\\\"version=n,b\\\\\"' not in found or '%command%' not in found:
lines[found_idx]='\t'*6+'\"LaunchOptions\"\t\t\"'+LAUNCH+'\"\n'; print(f'[*] {lc}: fixed escaping/truncated'); patched+=1
else:
import re as _re
m=_re.search(r'\"LaunchOptions\"\s+\"([^\"]*)\"', found)
ex=m.group(1) if m else ''
exu=ex.replace('\\\\\"','\"')
if 'WINEDLLOVERRIDES' in exu:
new=exu.replace('winhttp=n,b','version=n,b').replace('winhttp=n','version=n,b')
if 'version=n,b' not in new:
new=f'WINEDLLOVERRIDES=\"version=n,b\" {exu}' if '%command%' not in exu else exu.replace('%command%','WINEDLLOVERRIDES=\"version=n,b\" %command%') if 'WINEDLLOVERRIDES' not in exu.split('%command%')[0] else exu
newe=new.replace('\"','\\\\\"')
lines[found_idx]='\t'*6+'\"LaunchOptions\"\t\t\"'+newe+'\"\n'; print(f'[*] {lc}: updated winhttp -> version'); patched+=1
else:
if '%command%' in exu: combined=exu.replace('%command%','WINEDLLOVERRIDES=\"version=n,b\" %command%') if exu.strip() and 'WINEDLLOVERRIDES' not in exu else f'WINEDLLOVERRIDES=\"version=n,b\" %command%'
else: combined=f'WINEDLLOVERRIDES=\"version=n,b\" {exu}' if exu.strip() else 'WINEDLLOVERRIDES=\"version=n,b\" %command%'
combe=combined.replace('\"','\\\\\"')
lines[found_idx]='\t'*6+'\"LaunchOptions\"\t\t\"'+combe+'\"\n'; print(f'[*] {lc}: prepended version'); patched+=1
else:
lines.insert(end, '\t'*6+'\"LaunchOptions\"\t\t\"'+LAUNCH+'\"\n'); print(f'[*] {lc}: inserted LaunchOptions'); patched+=1
i=end+1+(1 if found is None else 0); continue
i+=1
if patched: p.write_text(''.join(lines)); print(f'[+] {lc}: patched {patched} block(s)')
else: print(f'[=] {lc}: no Apps 1274570 block')
" "$lc" 2>&1 | head -n 10
[[ $? -eq 0 ]] && patched_any=1
fi
done
if [[ $patched_any -eq 0 ]]; then echo "[!] No localconfig.vdf with Apps->1274570 found — launch once from Steam, then re-run install.sh"; fi if [[ $patched_any -eq 0 ]]; then echo "[!] No localconfig.vdf with Apps->1274570 found — launch once from Steam, then re-run install.sh"; fi
echo "[*] Linux/Proton: version.dll is sole unlocker beside DEVOUR.exe — Steam Launch Options now auto-set, just press Play. If Steam was running during this install: steam -shutdown, re-run install.sh, reopen Steam." echo "[*] Linux/Proton: version.dll is sole unlocker beside DEVOUR.exe — Steam Launch Options now auto-set, just press Play (restart Steam if it was running). No manual WINEDLLOVERRIDES needed."
echo "[*] Fallback: bash $(dirname "$0")/launch.sh (also auto-sets WINEDLLOVERRIDES)" echo "[*] Fallback: bash $(dirname "$0")/launch.sh (also auto-sets WINEDLLOVERRIDES)"
else else
echo "[*] Windows: version.dll beside DEVOUR.exe is auto-loaded (UnityPlayer.dll imports VERSION.dll) — no launch option needed. Just launch Devour." echo "[*] Windows: version.dll beside DEVOUR.exe is auto-loaded (UnityPlayer.dll imports VERSION.dll) — no launch option needed. Just launch Devour."

View file

@ -1,66 +0,0 @@
#!/usr/bin/env python3
"""Persist a Wine DllOverrides entry ("version"="native,builtin") inside the
Devour Proton prefix registry (user.reg).
Steam cannot overwrite this (it only manages its own LaunchOptions), so the
unlocker loads on every launch with zero user configuration.
Must run while the game/wineserver is NOT running (wineserver writes user.reg
back on clean exit).
"""
import pathlib, re, sys
APPID = "1274570"
SECTION = '[Software\\\\Wine\\\\DllOverrides]' # literal double backslash in reg file
def find_prefixes(steam_home: pathlib.Path):
out = []
direct = steam_home / f"steamapps/compatdata/{APPID}/pfx/user.reg"
if direct.exists():
out.append(direct)
lf = steam_home / "steamapps/libraryfolders.vdf"
if lf.exists():
for m in re.finditer(r'"path"\s+"([^"]+)"', lf.read_text(errors="ignore")):
cand = pathlib.Path(m.group(1)) / f"steamapps/compatdata/{APPID}/pfx/user.reg"
if cand.exists() and cand not in out:
out.append(cand)
return out
def patch(user_reg: pathlib.Path) -> bool:
t = user_reg.read_text(encoding="utf-8", errors="ignore")
changed = False
if SECTION in t:
i = t.index(SECTION)
j = t.find("\n[", i + 1)
end = j if j != -1 else len(t)
body = t[i:end]
if '"version"' in body:
new_body = re.sub(r'"version"="[^"]*"', '"version"="native,builtin"', body)
if new_body != body:
changed = True
else:
new_body = body.rstrip("\n") + '\n"version"="native,builtin"\n\n'
changed = True
if changed:
t = t[:i] + new_body + t[end:]
else:
t = t.rstrip("\n") + "\n\n" + SECTION + ' 1788000000\n"version"="native,builtin"\n\n'
changed = True
if changed:
bak = user_reg.with_suffix(".reg.bak.devour")
bak.write_text(t, encoding="utf-8")
user_reg.write_text(t, encoding="utf-8")
print(f"[+] {user_reg}: version=native,builtin set ({'updated' if 'updated' in locals() else 'added'})")
else:
print(f"[=] {user_reg}: already native,builtin")
return changed
if __name__ == "__main__":
home = pathlib.Path.home()
steam = home / ".local/share/Steam"
prefixes = find_prefixes(steam)
if not prefixes:
print("[!] No Proton prefix for Devour found (launch the game once via Steam first)")
sys.exit(0)
for p in prefixes:
patch(p)

View file

@ -1,76 +0,0 @@
#!/usr/bin/env python3
"""Patch Steam localconfig.vdf LaunchOptions for Devour (1274570).
Sets WINEDLLOVERRIDES="version=n,b" %command% inside every userdata/*/config/
localconfig.vdf Apps->1274570 block. Must be run while Steam is CLOSED — Steam
rewrites localconfig.vdf from memory on exit and discards external edits.
VDF escaping note: quotes inside values are a single backslash + quote (\").
Getting that through bash heredocs is what broke account 919954036 before
(it ended up as \\\"). This module builds bytes explicitly via chr(92).
"""
import pathlib, sys
APPID = "1274570"
BQ = chr(92) + '"' # backslash + quote
TARGET_LINE = (
"\t" * 6 + '"LaunchOptions"' + "\t\t"
+ '"WINEDLLOVERRIDES=' + BQ + 'version=n,b' + BQ + ' %command%"' + "\n"
)
def patch(path: pathlib.Path) -> bool:
lines = path.read_text(errors="ignore").splitlines(True)
changed = False
i = 0
while i < len(lines) - 1:
if (
lines[i].strip() == f'"{APPID}"'
and lines[i].startswith("\t" * 5)
and lines[i + 1].strip() == "{"
and lines[i + 1].startswith("\t" * 5)
):
depth = 1
j = i + 2
lo_i = None
end = None
while j < len(lines):
s = lines[j].strip()
if s == "{":
depth += 1
elif s == "}":
depth -= 1
if depth == 0:
end = j
break
if s.startswith('"LaunchOptions"') and lines[j].startswith("\t" * 6):
lo_i = j
j += 1
if end is None:
i = j + 1
continue
if lo_i is None:
lines.insert(end, TARGET_LINE)
print(f"{path.parent.parent.name}: inserted LaunchOptions")
changed = True
elif lines[lo_i] != TARGET_LINE:
print(f"{path.parent.parent.name}: fixed {lines[lo_i].strip()[:70]!r}")
lines[lo_i] = TARGET_LINE
changed = True
else:
print(f"{path.parent.parent.name}: already exact")
i = end + 1
continue
i += 1
if changed:
bak = path.with_suffix(".vdf.bak.devour")
bak.write_text("".join(lines))
path.write_text("".join(lines))
return changed
if __name__ == "__main__":
paths = [pathlib.Path(a) for a in sys.argv[1:]] or list(
pathlib.Path.home().glob(".local/share/Steam/userdata/*/config/localconfig.vdf")
)
for p in paths:
if p.exists():
patch(p)

View file

@ -17,10 +17,6 @@
namespace devour { namespace devour {
static std::set<uint64_t> g_queriedDefs;
std::set<uint64_t>& QueriedDefs(){ return g_queriedDefs; }
void RememberQueriedDef(uint64_t id){ g_queriedDefs.insert(id); }
#ifdef _WIN32 #ifdef _WIN32
static HMODULE g_gameAsm = nullptr; static HMODULE g_gameAsm = nullptr;
@ -365,227 +361,6 @@ void Il2CppResolver::dumpDiagnostics(){
#endif #endif
} }
void Il2CppResolver::inspectInventoryState(){
#ifdef _WIN32
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 << "[inv] inspectInventoryState BEGIN\n" << std::unitbuf;
auto domain_get = reinterpret_cast<void*(*)()>(getProc("il2cpp_domain_get"));
auto klass_from_name = reinterpret_cast<void*(*)(void*, const char*, const char*)>(getProc("il2cpp_class_from_name"));
auto field_from_name = reinterpret_cast<void*(*)(void*, const char*)>(getProc("il2cpp_class_get_field_from_name"));
auto field_static_get = reinterpret_cast<void(*)(void*, void*)>(getProc("il2cpp_field_static_get_value")); // (field, out)
auto field_get_value = reinterpret_cast<void(*)(void*, void*, void*)>(getProc("il2cpp_field_get_value"));
auto obj_get_class = reinterpret_cast<void*(*)(void*)>(getProc("il2cpp_object_get_class"));
auto class_get_name_fn = reinterpret_cast<const char*(*)(void*)>(getProc("il2cpp_class_get_name"));
auto arr_elem_size_cls = reinterpret_cast<int(*)(void*)>(getProc("il2cpp_class_array_element_size"));
auto fields_iter = reinterpret_cast<void*(*)(void*, void**)>(getProc("il2cpp_class_get_fields"));
auto field_name_fn = reinterpret_cast<const char*(*)(void*)>(getProc("il2cpp_field_get_name"));
auto field_off = reinterpret_cast<size_t(*)(void*)>(getProc("il2cpp_field_get_offset"));
if(!domain_get || !klass_from_name || !field_from_name || !field_static_get || !field_get_value || !obj_get_class || !class_get_name_fn){
f << "[inv] missing exports\n"; return;
}
void* domain = domain_get();
if(!domain){ f << "[inv] no domain\n"; return; }
// find SteamInventoryManager class across assemblies
auto dom_asm = reinterpret_cast<const void**(*)(void*, size_t*)>(getProc("il2cpp_domain_get_assemblies"));
auto asm_img = reinterpret_cast<void*(*)(void*)>(getProc("il2cpp_assembly_get_image"));
if(!dom_asm || !asm_img){ f << "[inv] no asm exports\n"; return; }
size_t n=0; const void** asms = dom_asm(domain,&n);
void* simClass=nullptr;
for(size_t i=0;i<n && !simClass;++i){
void* img=asm_img((void*)asms[i]); if(!img) continue;
simClass = klass_from_name(img,"","SteamInventoryManager");
}
if(!simClass){ f << "[inv] SteamInventoryManager class not found\n"; return; }
f << "[inv] SIM class " << class_get_name_fn(simClass) << "\n";
void* fInst = field_from_name(simClass,"instance");
if(!fInst){ f << "[inv] no 'instance' field\n"; return; }
void* mgr=nullptr;
field_static_get(fInst,&mgr);
if(!mgr){ f << "[inv] manager instance null (not Awake yet)\n"; return; }
f << "[inv] manager=" << mgr << "\n";
const char* containers[] = {"userItems","steamItems","m_SteamItemDetails",nullptr};
for(int ci=0;containers[ci];++ci){
void* fld = field_from_name(simClass,containers[ci]);
if(!fld){ f << "[inv] field "<<containers[ci]<<" missing\n"; continue; }
void* val=nullptr;
field_get_value(mgr,fld,&val);
if(!val){ f << "[inv] "<<containers[ci]<<" = null\n"; continue; }
void* vcls=obj_get_class(val);
f << "[inv] "<<containers[ci]<<" obj="<<val<<" class="<<(vcls?class_get_name_fn(vcls):"?\n");
f << "\n";
// If List<T>: _items @0x10, _size @0x18
unsigned char* lst=(unsigned char*)val;
void* arr = *(void**)(lst+0x10);
int32_t lsize = *(int32_t*)(lst+0x18);
f << "[inv] _items="<<arr<<" _size="<<lsize<<"\n";
if(!arr){ continue; }
void* arrCls = *(void**)arr;
int esz = arrCls && arr_elem_size_cls ? arr_elem_size_cls(arrCls) : 0;
const char* aname = arrCls? class_get_name_fn(arrCls):"?";
uint64_t maxlen = *(uint64_t*)((unsigned char*)arr+0x18);
f << "[inv] array class="<<aname<<" elemSize="<<esz<<" capacity="<<maxlen<<"\n";
}
f << "[inv] inspect END\n"; f.flush();
} catch(...){
try{ char e2[MAX_PATH]; GetModuleFileNameA(nullptr,e2,MAX_PATH); char* s2=strrchr(e2,'\\'); if(!s2) s2=strrchr(e2,'/'); if(s2){ std::ofstream f2(std::string(e2,s2-e2)+"\\DevourUnlocker.log",std::ios::app); f2<<"[inv] EXCEPTION during inspect (guarded)\n"; } }catch(...){}
}
#endif
}
int Il2CppResolver::fabricateOwnership(){
#ifdef _WIN32
int injected = -1; // -1 = not ready / error
try{
auto domain_get = reinterpret_cast<void*(*)()>(getProc("il2cpp_domain_get"));
auto thread_attach = reinterpret_cast<void*(*)(void*)>(getProc("il2cpp_thread_attach"));
auto klass_from_name = reinterpret_cast<void*(*)(void*, const char*, const char*)>(getProc("il2cpp_class_from_name"));
auto field_from_name = reinterpret_cast<void*(*)(void*, const char*)>(getProc("il2cpp_class_get_field_from_name"));
auto field_static_get = reinterpret_cast<void(*)(void*, void*)>(getProc("il2cpp_field_static_get_value"));
auto field_get_value = reinterpret_cast<void(*)(void*, void*, void*)>(getProc("il2cpp_field_get_value"));
auto obj_get_class = reinterpret_cast<void*(*)(void*)>(getProc("il2cpp_object_get_class"));
auto class_get_name_fn = reinterpret_cast<const char*(*)(void*)>(getProc("il2cpp_class_get_name"));
auto get_method = reinterpret_cast<void*(*)(void*, const char*, int)>(getProc("il2cpp_class_get_method_from_name"));
auto invoke_fn = reinterpret_cast<void*(*)(void*, void*, void**, void**)>(getProc("il2cpp_runtime_invoke"));
if(!domain_get || !thread_attach || !klass_from_name || !field_from_name || !field_static_get ||
!field_get_value || !obj_get_class || !class_get_name_fn || !get_method || !invoke_fn) return -1;
void* domain = domain_get();
if(!domain) return -1;
if(thread_attach) thread_attach(domain);
auto dom_asm = reinterpret_cast<const void**(*)(void*, size_t*)>(getProc("il2cpp_domain_get_assemblies"));
auto asm_img = reinterpret_cast<void*(*)(void*)>(getProc("il2cpp_assembly_get_image"));
if(!dom_asm || !asm_img) return -1;
size_t n=0; const void** asms = dom_asm(domain,&n);
// SteamInventoryManager class + Steamworks.SteamItemDef_t enum class
void* simClass=nullptr; void* defEnumClass=nullptr;
for(size_t i=0;i<n;++i){
void* img=asm_img((void*)asms[i]); if(!img) continue;
if(!simClass) simClass = klass_from_name(img,"","SteamInventoryManager");
if(!defEnumClass) defEnumClass = klass_from_name(img,"Steamworks","SteamItemDef_t");
if(simClass && defEnumClass) break;
}
if(!simClass) return -1;
void* fInst = field_from_name(simClass,"instance");
if(!fInst) return -1;
void* mgr=nullptr;
field_static_get(fInst,&mgr);
if(!mgr) return -1;
void* fUser = field_from_name(simClass,"userItems");
void* fCat = field_from_name(simClass,"steamItems");
if(!fUser || !fCat) return -1;
void* userList=nullptr; void* catList=nullptr;
field_get_value(mgr,fUser,&userList);
field_get_value(mgr,fCat,&catList);
if(!userList || !catList) return -1;
unsigned char* ul=(unsigned char*)userList;
void* uArr=*(void**)(ul+0x10); int32_t uSize=*(int32_t*)(ul+0x18);
unsigned char* cl=(unsigned char*)catList;
void* cArr=*(void**)(cl+0x10); int32_t cSize=*(int32_t*)(cl+0x18);
if(!cArr || cSize<=0) return -1;
// existing owned ids (List<SteamItemDef_t>, enum is 4-byte int despite array_element_size saying 8)
std::vector<uint64_t> have;
if(uArr){
unsigned char* data=(unsigned char*)uArr+0x20;
for(int i=0;i<uSize;++i) have.push_back((uint64_t)*(uint32_t*)(data+(size_t)i*4));
}
auto hasId=[&](uint64_t id){ for(auto v:have) if(v==id) return true; return false; };
if(uSize >= cSize && cSize>0) return 0; // already fully populated
void* addMethod = get_method(obj_get_class(userList),"Add",1);
if(!addMethod) return -1;
unsigned char* cData=(unsigned char*)cArr+0x20;
// DIAGNOSTIC: hex-dump sample ids so mismatches are visible
auto dumpIds=[&](const char* tag){
try{
char exe[MAX_PATH]; GetModuleFileNameA(nullptr,exe,MAX_PATH);
char* s=strrchr(exe,'\\'); if(!s) s=strrchr(exe,'/'); if(!s) return;
std::ofstream f(std::string(exe,s-exe)+"\\DevourUnlocker.log",std::ios::app);
f<<"[dbg] "<<tag<<":";
if(strcmp(tag,"have")==0){ for(size_t i=0;i<have.size()&&i<8;++i) f<<" "<<std::hex<<(unsigned long long)have[i]; }
else { for(int i=0;i<cSize&&i<8;++i){ void* ip=*(void**)(cData+(size_t)i*8); if(!ip) continue; uint64_t d=(uint64_t)*(uint32_t*)((unsigned char*)ip+0x10); f<<" "<<std::hex<<(unsigned long long)d; } }
f<<std::dec<<"\n";
}catch(...){}
};
dumpIds("have");
dumpIds("catalog");
injected=0;
int verified=0;
for(int i=0;i<cSize;++i){
void* itemPtr=*(void**)(cData+(size_t)i*8);
if(!itemPtr) continue;
uint64_t defId=(uint64_t)*(uint32_t*)((unsigned char*)itemPtr+0x10); // SteamInventoryItem.itemID @+0x10 (int-backed)
if(defId==0 || hasId(defId)) continue;
void* exc=nullptr; void* arg=&defId;
invoke_fn(addMethod,userList,&arg,&exc);
if(exc){ continue; }
have.push_back(defId);
++injected;
// verify first 3 adds actually stored the right value
if(verified<3){
void* uArrNow=*(void**)(ul+0x10); int32_t szNow=*(int32_t*)(ul+0x18);
if(uArrNow && szNow>0){
unsigned char* dNow=(unsigned char*)uArrNow+0x20;
uint64_t last=(uint64_t)*(uint32_t*)(dNow+(size_t)(szNow-1)*4);
try{
char exe[MAX_PATH]; GetModuleFileNameA(nullptr,exe,MAX_PATH);
char* s=strrchr(exe,'\\'); if(!s) s=strrchr(exe,'/');
if(s){ std::ofstream f(std::string(exe,s-exe)+"\\DevourUnlocker.log",std::ios::app);
f<<std::hex<<"[dbg] add sent="<<(unsigned long long)defId<<" stored_last="<<(unsigned long long)last<<" size="<<std::dec<<szNow<<"\n"; }
}catch(...){}
}
++verified;
}
}
try{
char exe[MAX_PATH]; GetModuleFileNameA(nullptr,exe,MAX_PATH);
char* s=strrchr(exe,'\\'); if(!s) s=strrchr(exe,'/');
if(s){
std::ofstream f(std::string(exe,s-exe)+"\\DevourUnlocker.log",std::ios::app);
static bool loggedCatalog=false;
if(!loggedCatalog && cSize>0){
loggedCatalog=true;
f<<"[own] catalog ids:";
for(int i=0;i<cSize;++i){ void* ip=*(void**)(cData+(size_t)i*8); if(!ip) continue; f<<" "<<*(uint32_t*)((unsigned char*)ip+0x10); }
f<<"\n";
}
// Self-learning: inject any def the game queried but that lives outside the
// store catalog (Supporter/DLC flashlights etc). Valid ids only - no sweeps.
int learned=0;
for(uint64_t qid : g_queriedDefs){
if(qid==0 || hasId(qid)) continue;
void* exc=nullptr; void* arg=&qid;
invoke_fn(addMethod,userList,&arg,&exc);
if(exc) continue;
have.push_back(qid);
++learned;
}
f<<"[own] fabricate: catalog="<<cSize<<" had="<<(uSize>0?uSize:(int)(have.size()-injected))<<" injected="<<injected<<" learned="<<learned<<"\n";
}
}catch(...){}
} catch(...){ injected=-1; }
return injected;
#else
return -1;
#endif
}
#else // Linux stubs #else // Linux stubs
uintptr_t Il2CppResolver::gameAssemblyBase() { return 0; } uintptr_t Il2CppResolver::gameAssemblyBase() { return 0; }

View file

@ -2,7 +2,6 @@
#include <cstdint> #include <cstdint>
#include <string> #include <string>
#include <vector> #include <vector>
#include <set>
// Minimal IL2CPP types — avoids needing il2cpp headers at compile time // Minimal IL2CPP types — avoids needing il2cpp headers at compile time
struct Il2CppString { void* klass; void* monitor; int32_t length; wchar_t chars[1]; }; struct Il2CppString { void* klass; void* monitor; int32_t length; wchar_t chars[1]; };
@ -10,12 +9,6 @@ struct MethodInfo;
namespace devour { namespace devour {
// Def ids the game itself asked UserHasItem/PlayerHasItem about (DLC/promo defs
// absent from the store catalog live here). Fabricator injects these too.
std::set<uint64_t>& QueriedDefs();
void RememberQueriedDef(uint64_t id);
// Resolves IL2CPP method pointers without hard offsets. // Resolves IL2CPP method pointers without hard offsets.
// Strategy 1: via il2cpp exports (class/method lookup) // Strategy 1: via il2cpp exports (class/method lookup)
// Strategy 2: xref string sigscan as fallback // Strategy 2: xref string sigscan as fallback
@ -52,15 +45,6 @@ public:
static ResolvedMethod playerHasItem(); // SteamInventoryValidator.PlayerHasItem(connId, steamItemDefID) static ResolvedMethod playerHasItem(); // SteamInventoryValidator.PlayerHasItem(connId, steamItemDefID)
static ResolvedMethod hasRetrievedUserItems(); // SteamInventoryManager.HasRetrievedUserInventoryItems() static ResolvedMethod hasRetrievedUserItems(); // SteamInventoryManager.HasRetrievedUserInventoryItems()
static void dumpDiagnostics(); static void dumpDiagnostics();
// Raw introspection of SteamInventoryManager singleton containers (userItems etc).
// Logs element type/size/sample bytes so we can fabricate ownership entries safely.
static void inspectInventoryState();
// Injects every catalog def id from steamItems into userItems (List<SteamItemDef_t>::Add).
// Returns number newly injected. Safe to call repeatedly — the game recreates the
// list on inventory refresh, so run this periodically.
static int fabricateOwnership();
// Accessors used by the inspector/fabricator
static void* getProcPub(const char* name) { return getProc(name); }
static uintptr_t gameAssemblyBase(); static uintptr_t gameAssemblyBase();
static size_t gameAssemblySize(); static size_t gameAssemblySize();
}; };

View file

@ -66,18 +66,9 @@ static std::string il2cppStringToUtf8(void* ilStr) {
} }
static bool __fastcall hIsRobe(void* thiz, void* robeStr, void* characterStr, void* method) { return true; } static bool __fastcall hIsRobe(void* thiz, void* robeStr, void* characterStr, void* method) { return true; }
static bool __fastcall hIsChar(void* thiz, void* charStr, void* method) { static bool __fastcall hIsChar(void* thiz, void* charStr, void* method) { return true; }
logPatch((std::string("[trace] IsCharacterUnlocked(\"") + il2cppStringToUtf8(charStr) + "\") -> true").c_str()); static bool __fastcall hHasPerk(void* thiz, void* perkStr, void* method) { return true; }
return true; static bool __fastcall hHasOutfit(void* thiz, void* outfitStr, void* characterStr, void* method) { return true; }
}
static bool __fastcall hHasPerk(void* thiz, void* perkStr, void* method) {
logPatch((std::string("[trace] HasPerk(\"") + il2cppStringToUtf8(perkStr) + "\") -> true").c_str());
return true;
}
static bool __fastcall hHasOutfit(void* thiz, void* outfitStr, void* characterStr, void* method) {
logPatch((std::string("[trace] HasOutfit(\"") + il2cppStringToUtf8(outfitStr) + "\",\"" + il2cppStringToUtf8(characterStr) + "\") -> true").c_str());
return true;
}
// UpdateLocked*: call the ORIGINAL so the UI runs its normal flow — ownership checks // UpdateLocked*: call the ORIGINAL so the UI runs its normal flow — ownership checks
// (UserHasItem→true) make it mark everything unlocked instead of leaving defaults locked // (UserHasItem→true) make it mark everything unlocked instead of leaving defaults locked
static void __fastcall hUpdateLockedPets(void* thiz, void* method) { if (oUpdPets) oUpdPets(thiz, method); } static void __fastcall hUpdateLockedPets(void* thiz, void* method) { if (oUpdPets) oUpdPets(thiz, method); }
@ -87,32 +78,20 @@ static void __fastcall hUpdateLockedFlashlights(void* thiz, void* method) { if (
static void __fastcall hUpdateLockedEmotes(void* thiz, void* method) { if (oUpdEmotes) oUpdEmotes(thiz, method); } static void __fastcall hUpdateLockedEmotes(void* thiz, void* method) { if (oUpdEmotes) oUpdEmotes(thiz, method); }
static void __fastcall hUpdateLockedOutfits(void* thiz, void* method) { if (oUpdOutfits) oUpdOutfits(thiz, method); } static void __fastcall hUpdateLockedOutfits(void* thiz, void* method) { if (oUpdOutfits) oUpdOutfits(thiz, method); }
static void __fastcall hUpdateLockedRobes(void* thiz, void* method) { if (oUpdRobes) oUpdRobes(thiz, method); } static void __fastcall hUpdateLockedRobes(void* thiz, void* method) { if (oUpdRobes) oUpdRobes(thiz, method); }
// Ownership override: every Steam catalog item (pets/flashlights/emotes/outfits/promos) counts as owned. // Ownership override: every Steam catalog item (pets/flashlights/emotes/outfits/promos) counts as owned
// LOGS every query — if the pet UI never appears in the log, it reads the userItems list field directly.
static bool __fastcall hUserHasItem(void* thiz, unsigned long long itemDef, void* method) { static bool __fastcall hUserHasItem(void* thiz, unsigned long long itemDef, void* method) {
// pass-through + trace (v2.1.0 proved forcing true blocks purchases) (void)itemDef;
RememberQueriedDef((uint64_t)itemDef); return true;
char b[96]; snprintf(b,sizeof(b),"[trace] UserHasItem(def=%llu)", (unsigned long long)itemDef); logPatch(b);
if (oUserHasItem) return oUserHasItem(thiz, itemDef, method);
return false;
} }
// Multiplayer validator: every player counts as owning every item (lobby validation path) // Multiplayer validator: every player counts as owning every item (lobby validation path)
static bool __fastcall hPlayerHasItem(void* thiz, int connectionId, unsigned long long itemDef, void* method) { static bool __fastcall hPlayerHasItem(void* thiz, int connectionId, unsigned long long itemDef, void* method) {
// MUST stay pass-through: forcing true desyncs lobby validation vs injected (void)connectionId; (void)itemDef;
// userItems and UNLOCKS REGRESS (pets locked again). See v2.1.1 regression.
RememberQueriedDef((uint64_t)itemDef);
char b[128]; snprintf(b,sizeof(b),"[trace] PlayerHasItem(conn=%d def=%llu)", connectionId, (unsigned long long)itemDef); logPatch(b);
if (oPlayerHasItem) return oPlayerHasItem(thiz, connectionId, itemDef, method);
return false;
}
// Inventory-retrieved flag: pass through — forcing true with an empty userItems made some
// flows treat items as definitively NOT owned; original behavior is safer now
static bool __fastcall hHasRetrievedUserItems(void* thiz, void* method) {
if (oHasRetrieved) return oHasRetrieved(thiz, method);
return true; return true;
} }
// Perks bought with Tokens: huge balance — perk modal deducts locally, so every perk // Inventory-retrieved flag: unblocks PopulateInventoryWhenAvailable even if the Steam
// is instantly affordable. Store items don't use tokens (they route through Steam). // inventory query returns nothing under Proton — views populate with UserHasItem=true
static bool __fastcall hHasRetrievedUserItems(void* thiz, void* method) { return true; }
// Perks bought with Tokens: report a huge balance so every perk is purchasable/selectable
static int __fastcall hGetCurrentTokens(void* thiz, void* method) { return 9999999; } static int __fastcall hGetCurrentTokens(void* thiz, void* method) { return 9999999; }
static bool hookUpdateLocked(void* addr, const char* name, bool shouldUnlock, void (__fastcall *hook)(void*, void*), UpdateLockedFn* origOut) { static bool hookUpdateLocked(void* addr, const char* name, bool shouldUnlock, void (__fastcall *hook)(void*, void*), UpdateLockedFn* origOut) {
@ -181,11 +160,6 @@ bool installPatches() {
char buf[1024]; 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); snprintf(buf, sizeof(buf), "[DevourUnlocker] Resolved IsRobe=%p IsChar=%p HasPerk=%p HasOutfit=%p", robe.address, ch.address, hasPerk.address, hasOutfit.address);
logPatch(buf); logPatch(buf);
{
HMODULE ga = GetModuleHandleA("GameAssembly.dll");
char b3[160]; snprintf(b3,sizeof(b3),"[rva] GameAssembly base=%p", (void*)ga);
logPatch(b3);
}
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); 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); logPatch(buf);
} }
@ -333,25 +307,6 @@ bool installPatches() {
} }
g_installed = any; g_installed = any;
logPatch(any ? "[DevourUnlocker] Patches installed (some may retry)" : "[DevourUnlocker] No patches installed"); logPatch(any ? "[DevourUnlocker] Patches installed (some may retry)" : "[DevourUnlocker] No patches installed");
// Ownership fabricator + introspection thread:
// - every 15s inject missing catalog ids into userItems (list is recreated on refresh)
// - one layout dump at 30s for diagnostics
CreateThread(nullptr, 0, [](LPVOID)->DWORD{
Sleep(30000);
logPatch("[DevourUnlocker] inspectInventoryState @30s");
Il2CppResolver::inspectInventoryState();
int total=0;
for(int cycle=0; cycle<240; ++cycle){ // ~60 min of refills
Sleep(15000);
int n = Il2CppResolver::fabricateOwnership();
if(n>0){
total+=n;
char b[128]; snprintf(b,sizeof(b),"[DevourUnlocker] ownership injected +%d (session total %d)",n,total);
logPatch(b);
}
}
return 0;
}, nullptr, 0, nullptr);
return any; return any;
} }