Compare commits
4 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ecf1e680d6 | ||
|
|
817a2eff78 | ||
|
|
fee1556da3 | ||
|
|
f478a731d9 |
5 changed files with 123 additions and 11 deletions
|
|
@ -144,6 +144,13 @@ if [[ "$OS" == "linux" ]]; then
|
|||
echo "[!] Steam is RUNNING — it will overwrite localconfig.vdf on exit and drop our LaunchOptions."
|
||||
echo " Close Steam first: steam -shutdown then re-run install.sh"
|
||||
fi
|
||||
# PRIMARY (Steam-proof): per-prefix Wine registry override — Steam never touches user.reg
|
||||
if [[ -f "$(dirname "$0")/prefix_override.py" ]]; then
|
||||
python3 "$(dirname "$0")/prefix_override.py" 2>&1 | head -n 10
|
||||
else
|
||||
echo "[!] prefix_override.py missing — registry override not applied"
|
||||
fi
|
||||
# SECONDARY: Steam LaunchOptions (harmless if Steam drops it later)
|
||||
if [[ -f "$(dirname "$0")/vdf_patch.py" ]]; then
|
||||
python3 "$(dirname "$0")/vdf_patch.py" 2>&1 | head -n 20 && patched_any=1
|
||||
else
|
||||
|
|
|
|||
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)
|
||||
|
|
@ -17,6 +17,10 @@
|
|||
|
||||
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
|
||||
static HMODULE g_gameAsm = nullptr;
|
||||
|
||||
|
|
@ -553,7 +557,26 @@ int Il2CppResolver::fabricateOwnership(){
|
|||
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<<"[own] fabricate: catalog="<<cSize<<" had="<<(uSize>0?uSize:(int)have.size()-injected)<<" injected="<<injected<<"\n";
|
||||
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; }
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <set>
|
||||
|
||||
// Minimal IL2CPP types — avoids needing il2cpp headers at compile time
|
||||
struct Il2CppString { void* klass; void* monitor; int32_t length; wchar_t chars[1]; };
|
||||
|
|
@ -9,6 +10,12 @@ struct MethodInfo;
|
|||
|
||||
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.
|
||||
// Strategy 1: via il2cpp exports (class/method lookup)
|
||||
// Strategy 2: xref string sigscan as fallback
|
||||
|
|
|
|||
|
|
@ -90,14 +90,20 @@ static void __fastcall hUpdateLockedRobes(void* thiz, void* method) { if (oUpdRo
|
|||
// 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 thread_local int depth = 0;
|
||||
if (depth < 2) { depth++; char b[128]; snprintf(b,sizeof(b),"[trace] UserHasItem(def=%llu) -> true", itemDef); logPatch(b); depth--; }
|
||||
return true;
|
||||
// pass-through + trace (v2.1.0 proved forcing true blocks purchases)
|
||||
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)
|
||||
static bool __fastcall hPlayerHasItem(void* thiz, int connectionId, unsigned long long itemDef, void* method) {
|
||||
char b[160]; snprintf(b,sizeof(b),"[trace] PlayerHasItem(conn=%d def=%llu) -> true", connectionId, itemDef); logPatch(b);
|
||||
return true;
|
||||
// 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
|
||||
|
|
@ -105,11 +111,9 @@ static bool __fastcall hHasRetrievedUserItems(void* thiz, void* method) {
|
|||
if (oHasRetrieved) return oHasRetrieved(thiz, method);
|
||||
return true;
|
||||
}
|
||||
// Perks bought with Tokens: PASS THROUGH — spoofed 9,999,999 broke the token purchase modal.
|
||||
static int __fastcall hGetCurrentTokens(void* thiz, void* method) {
|
||||
if (oGetTokens) return oGetTokens(thiz, method);
|
||||
return 0;
|
||||
}
|
||||
// Perks bought with Tokens: huge balance — perk modal deducts locally, so every perk
|
||||
// is instantly affordable. Store items don't use tokens (they route through Steam).
|
||||
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) {
|
||||
if (!addr) { logPatch((std::string("[DevourUnlocker] ")+name+" not found").c_str()); return false; }
|
||||
|
|
@ -177,6 +181,11 @@ bool installPatches() {
|
|||
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);
|
||||
{
|
||||
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);
|
||||
logPatch(buf);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue