Compare commits
6 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ecf1e680d6 | ||
|
|
817a2eff78 | ||
|
|
fee1556da3 | ||
|
|
f478a731d9 | ||
|
|
7632978bf3 | ||
|
|
de4a4328ae |
6 changed files with 457 additions and 68 deletions
|
|
@ -138,65 +138,26 @@ 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
|
||||||
patched_any=0
|
if pgrep -x steam >/dev/null 2>&1; then
|
||||||
for lc in "$HOME/.local/share/Steam/userdata/"*/config/localconfig.vdf; do
|
echo "[!] Steam is RUNNING — it will overwrite localconfig.vdf on exit and drop our LaunchOptions."
|
||||||
if [[ -f "$lc" ]] && grep -q '"1274570"' "$lc"; then
|
echo " Close Steam first: steam -shutdown then re-run install.sh"
|
||||||
cp "$lc" "$lc.bak.$(date +%s)" 2>/dev/null || true
|
fi
|
||||||
python3 -c "
|
# PRIMARY (Steam-proof): per-prefix Wine registry override — Steam never touches user.reg
|
||||||
import pathlib, re, sys
|
if [[ -f "$(dirname "$0")/prefix_override.py" ]]; then
|
||||||
lc=sys.argv[1]
|
python3 "$(dirname "$0")/prefix_override.py" 2>&1 | head -n 10
|
||||||
LAUNCH='WINEDLLOVERRIDES=\\\\\"version=n,b\\\\\" %command%'
|
else
|
||||||
p=pathlib.Path(lc)
|
echo "[!] prefix_override.py missing — registry override not applied"
|
||||||
txt=p.read_text(errors='ignore')
|
fi
|
||||||
lines=txt.splitlines(True)
|
# SECONDARY: Steam LaunchOptions (harmless if Steam drops it later)
|
||||||
patched=0
|
if [[ -f "$(dirname "$0")/vdf_patch.py" ]]; then
|
||||||
i=0
|
python3 "$(dirname "$0")/vdf_patch.py" 2>&1 | head -n 20 && patched_any=1
|
||||||
while i < len(lines)-1:
|
else
|
||||||
if lines[i].strip()=='\"1274570\"' and lines[i].startswith('\t'*5) and lines[i+1].strip()=='{' and lines[i+1].startswith('\t'*5):
|
echo "[!] vdf_patch.py missing — cannot auto-set Launch Options"
|
||||||
start=i+1; depth=1; j=i+2; found=None; found_idx=None; end=None
|
fi
|
||||||
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 (restart Steam if it was running). No manual WINEDLLOVERRIDES needed."
|
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 "[*] 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."
|
||||||
|
|
|
||||||
66
scripts/prefix_override.py
Normal file
66
scripts/prefix_override.py
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
#!/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)
|
||||||
76
scripts/vdf_patch.py
Normal file
76
scripts/vdf_patch.py
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
#!/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)
|
||||||
|
|
@ -17,6 +17,10 @@
|
||||||
|
|
||||||
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;
|
||||||
|
|
||||||
|
|
@ -361,6 +365,227 @@ 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; }
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
#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]; };
|
||||||
|
|
@ -9,6 +10,12 @@ 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
|
||||||
|
|
@ -45,6 +52,15 @@ 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();
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -66,9 +66,18 @@ 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) { return true; }
|
static bool __fastcall hIsChar(void* thiz, void* charStr, void* method) {
|
||||||
static bool __fastcall hHasPerk(void* thiz, void* perkStr, void* method) { return true; }
|
logPatch((std::string("[trace] IsCharacterUnlocked(\"") + il2cppStringToUtf8(charStr) + "\") -> true").c_str());
|
||||||
static bool __fastcall hHasOutfit(void* thiz, void* outfitStr, void* characterStr, void* method) { return true; }
|
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); }
|
||||||
|
|
@ -78,20 +87,32 @@ 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) {
|
||||||
(void)itemDef;
|
// pass-through + trace (v2.1.0 proved forcing true blocks purchases)
|
||||||
return true;
|
RememberQueriedDef((uint64_t)itemDef);
|
||||||
|
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) {
|
||||||
(void)connectionId; (void)itemDef;
|
// MUST stay pass-through: forcing true desyncs lobby validation vs injected
|
||||||
|
// 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;
|
||||||
}
|
}
|
||||||
// Inventory-retrieved flag: unblocks PopulateInventoryWhenAvailable even if the Steam
|
// Perks bought with Tokens: huge balance — perk modal deducts locally, so every perk
|
||||||
// inventory query returns nothing under Proton — views populate with UserHasItem=true
|
// is instantly affordable. Store items don't use tokens (they route through Steam).
|
||||||
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) {
|
||||||
|
|
@ -160,6 +181,11 @@ 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);
|
||||||
}
|
}
|
||||||
|
|
@ -307,6 +333,25 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue