v2.0.9: full store unlock — hook the ownership validators themselves
Some checks failed
Build DevourUnlocker / windows-cross (push) Failing after 8s
Build DevourUnlocker / linux (push) Failing after 6s
Build DevourUnlocker / standalone (push) Successful in 5s

Metadata dig findings (Assembly-CSharp, 2349 types):
- SteamInventoryValidator.PlayerHasItem(connId, steamItemDefID): multiplayer/purchase
  validation gate used by WaitForPurchased{Pet,Emote,Flashlight,Outfit}Validation
  coroutines (<validator>5__4) -> hooked true
- SteamInventoryManager.HasRetrievedUserInventoryItems(): gates PopulateInventoryWhenAvailable;
  if Steam inventory query under Proton returns nothing, item views never populate ->
  hooked true so lobby tabs build with UserHasItem=true
- SteamInventoryManager.userItems cache read directly by UI in some paths — covered by
  UserHasItem+PlayerHasItem+HasRetrieved trio

Now 15 hooks: IsRobeUnlocked/IsCharacterUnlocked/HasPerk/HasOutfit->true,
UserHasItem/PlayerHasItem->owned, GetCurrentTokens->9999999,
HasRetrievedUserItems->true, UpdateLocked*(6)+WaitForPurchasedPetValidation call-through.
Pets/perks/flashlights/emotes/outfits route through the game's own grant path — no
purchase browser, no coin spoofing visible anywhere.
This commit is contained in:
MiTHRAL 2026-08-24 01:23:36 -04:00
parent 48d0edac0a
commit d9a50caa4f
3 changed files with 43 additions and 1 deletions

View file

@ -212,6 +212,8 @@ ResolvedMethod Il2CppResolver::updateLockedRobes() { return resolve("Menu", "Upd
ResolvedMethod Il2CppResolver::waitForPurchasedPet() { return resolve("Menu", "WaitForPurchasedPetValidation", 1); }
ResolvedMethod Il2CppResolver::userHasItem() { return resolve("SteamInventoryManager", "UserHasItem", 1); }
ResolvedMethod Il2CppResolver::getCurrentTokens() { return resolve("Menu", "GetCurrentTokens", 0); }
ResolvedMethod Il2CppResolver::playerHasItem() { return resolve("SteamInventoryValidator", "PlayerHasItem", 2); }
ResolvedMethod Il2CppResolver::hasRetrievedUserItems() { return resolve("SteamInventoryManager", "HasRetrievedUserInventoryItems", 0); }
void Il2CppResolver::dumpDiagnostics(){
#ifdef _WIN32
@ -382,6 +384,8 @@ ResolvedMethod Il2CppResolver::updateLockedRobes() { return {"UpdateLockedRobes"
ResolvedMethod Il2CppResolver::waitForPurchasedPet() { return {"WaitForPurchasedPetValidation", nullptr}; }
ResolvedMethod Il2CppResolver::userHasItem() { return {"UserHasItem", nullptr}; }
ResolvedMethod Il2CppResolver::getCurrentTokens() { return {"GetCurrentTokens", nullptr}; }
ResolvedMethod Il2CppResolver::playerHasItem() { return {"PlayerHasItem", nullptr}; }
ResolvedMethod Il2CppResolver::hasRetrievedUserItems() { return {"HasRetrievedUserInventoryItems", nullptr}; }
#endif

View file

@ -42,6 +42,8 @@ public:
static ResolvedMethod waitForPurchasedPet();
static ResolvedMethod userHasItem(); // SteamInventoryManager.UserHasItem(steamItemDefID)
static ResolvedMethod getCurrentTokens(); // Horror.Menu.GetCurrentTokens()
static ResolvedMethod playerHasItem(); // SteamInventoryValidator.PlayerHasItem(connId, steamItemDefID)
static ResolvedMethod hasRetrievedUserItems(); // SteamInventoryManager.HasRetrievedUserInventoryItems()
static void dumpDiagnostics();
static uintptr_t gameAssemblyBase();
static size_t gameAssemblySize();

View file

@ -29,6 +29,8 @@ using IsUnlockedFn1 = bool(__fastcall*)(void* thiz, void* str, void* method);
using UpdateLockedFn = void(__fastcall*)(void* thiz, void* method);
using UserHasItemFn = bool(__fastcall*)(void* thiz, unsigned long long itemDef, void* method);
using GetTokensFn = int(__fastcall*)(void* thiz, void* method);
using PlayerHasItemFn = bool(__fastcall*)(void* thiz, int connectionId, unsigned long long itemDef, void* method);
using HasRetrievedFn = bool(__fastcall*)(void* thiz, void* method);
static IsUnlockedFn oIsRobe = nullptr;
static IsUnlockedFn1 oIsChar = nullptr;
@ -43,6 +45,8 @@ static UpdateLockedFn oUpdRobes = nullptr;
static UpdateLockedFn oWaitPet = nullptr;
static UserHasItemFn oUserHasItem = nullptr;
static GetTokensFn oGetTokens = nullptr;
static PlayerHasItemFn oPlayerHasItem = nullptr;
static HasRetrievedFn oHasRetrieved = nullptr;
static bool g_installed = false;
static bool g_mhInit = false;
@ -79,6 +83,14 @@ static bool __fastcall hUserHasItem(void* thiz, unsigned long long itemDef, void
(void)itemDef;
return true;
}
// 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) {
(void)connectionId; (void)itemDef;
return true;
}
// Inventory-retrieved flag: unblocks PopulateInventoryWhenAvailable even if the 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; }
@ -196,8 +208,10 @@ bool installPatches() {
{
auto uhi = Il2CppResolver::userHasItem();
auto tok = Il2CppResolver::getCurrentTokens();
auto phi = Il2CppResolver::playerHasItem();
auto hri = Il2CppResolver::hasRetrievedUserItems();
char b2[512];
snprintf(b2, sizeof(b2), "[DevourUnlocker] Resolved UserHasItem=%p GetCurrentTokens=%p", uhi.address, tok.address);
snprintf(b2, sizeof(b2), "[DevourUnlocker] Resolved UserHasItem=%p GetCurrentTokens=%p PlayerHasItem=%p HasRetrievedUserItems=%p", uhi.address, tok.address, phi.address, hri.address);
logPatch(b2);
if (uhi.valid()) {
void* orig = nullptr;
@ -217,6 +231,24 @@ bool installPatches() {
any = true;
} else logPatch("[DevourUnlocker] Hook GetCurrentTokens failed");
} else logPatch("[DevourUnlocker] GetCurrentTokens not found");
if (phi.valid()) {
void* orig = nullptr;
if (MH_CreateHook(phi.address, (void*)hPlayerHasItem, &orig) == MH_OK) {
MH_EnableHook(phi.address);
oPlayerHasItem = reinterpret_cast<PlayerHasItemFn>(orig);
logPatch("[DevourUnlocker] Hooked PlayerHasItem -> always owned");
any = true;
} else logPatch("[DevourUnlocker] Hook PlayerHasItem failed");
} else logPatch("[DevourUnlocker] PlayerHasItem not found");
if (hri.valid()) {
void* orig = nullptr;
if (MH_CreateHook(hri.address, (void*)hHasRetrievedUserItems, &orig) == MH_OK) {
MH_EnableHook(hri.address);
oHasRetrieved = reinterpret_cast<HasRetrievedFn>(orig);
logPatch("[DevourUnlocker] Hooked HasRetrievedUserInventoryItems -> true");
any = true;
} else logPatch("[DevourUnlocker] Hook HasRetrievedUserInventoryItems failed");
} else logPatch("[DevourUnlocker] HasRetrievedUserInventoryItems not found");
}
any |= hookUpdateLocked(updPets.address, "UpdateLockedPets", Config::get().unlockPets, hUpdateLockedPets, &oUpdPets);
any |= hookUpdateLocked(updPerks.address, "UpdateLockedPerks", Config::get().unlockPerks, hUpdateLockedPerks, &oUpdPerks);
@ -245,6 +277,8 @@ bool installPatches() {
auto pw = Il2CppResolver::waitForPurchasedPet();
auto uh = Il2CppResolver::userHasItem();
auto tk = Il2CppResolver::getCurrentTokens();
auto ph = Il2CppResolver::playerHasItem();
auto hr = Il2CppResolver::hasRetrievedUserItems();
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"); } }
@ -252,6 +286,8 @@ bool installPatches() {
if(ho.valid()){ if(MH_CreateHook(ho.address,(void*)hHasOutfit,(void**)&oHasOutfit)==MH_OK){ MH_EnableHook(ho.address); any2=true; logPatch("[DevourUnlocker] Retry hooked HasOutfit"); } }
if(uh.valid()){ void* og=nullptr; if(MH_CreateHook(uh.address,(void*)hUserHasItem,&og)==MH_OK){ MH_EnableHook(uh.address); oUserHasItem=reinterpret_cast<UserHasItemFn>(og); any2=true; logPatch("[DevourUnlocker] Retry hooked UserHasItem"); } }
if(tk.valid()){ void* og=nullptr; if(MH_CreateHook(tk.address,(void*)hGetCurrentTokens,&og)==MH_OK){ MH_EnableHook(tk.address); oGetTokens=reinterpret_cast<GetTokensFn>(og); any2=true; logPatch("[DevourUnlocker] Retry hooked GetCurrentTokens"); } }
if(ph.valid()){ void* og=nullptr; if(MH_CreateHook(ph.address,(void*)hPlayerHasItem,&og)==MH_OK){ MH_EnableHook(ph.address); oPlayerHasItem=reinterpret_cast<PlayerHasItemFn>(og); any2=true; logPatch("[DevourUnlocker] Retry hooked PlayerHasItem"); } }
if(hr.valid()){ void* og=nullptr; if(MH_CreateHook(hr.address,(void*)hHasRetrievedUserItems,&og)==MH_OK){ MH_EnableHook(hr.address); oHasRetrieved=reinterpret_cast<HasRetrievedFn>(og); any2=true; logPatch("[DevourUnlocker] Retry hooked HasRetrievedUserInventoryItems"); } }
if(p1.valid()) any2 |= hookUpdateLocked(p1.address, "UpdateLockedPets[retry]", Config::get().unlockPets, hUpdateLockedPets, &oUpdPets);
if(p2.valid()) any2 |= hookUpdateLocked(p2.address, "UpdateLockedPerks[retry]", Config::get().unlockPerks, hUpdateLockedPerks, &oUpdPerks);
if(p3.valid()) any2 |= hookUpdateLocked(p3.address, "UpdateLockedFlashlights[retry]", Config::get().unlockFlashlights, hUpdateLockedFlashlights, &oUpdFlash);