DevourUnlocker/src/patches.cpp
MiTHRAL 817a2eff78
Some checks failed
Build DevourUnlocker / windows-cross (push) Failing after 7s
Build DevourUnlocker / linux (push) Failing after 7s
Build DevourUnlocker / standalone (push) Successful in 5s
v2.1.4: remove def-id range sweep — polluting userItems with unknown ids broke pet display; restore exact v2.1.0 injection semantics, keep pass-through traces + catalog dump
2026-08-24 13:48:35 -04:00

377 lines
22 KiB
C++

#include "patches.hpp"
#include "il2cpp_resolver.hpp"
#include "config.hpp"
#include <iostream>
#include <fstream>
#include <filesystem>
#ifdef _WIN32
#include <windows.h>
#include <MinHook.h>
#endif
static void logPatch(const char* s){
#ifdef _WIN32
try{
char exe[MAX_PATH]; GetModuleFileNameA(nullptr, exe, MAX_PATH);
std::filesystem::path p(exe); p = p.parent_path() / "DevourUnlocker.log";
std::ofstream f(p, std::ios::app); f << s << std::endl;
}catch(...){}
#endif
}
namespace devour {
#ifdef _WIN32
using IsUnlockedFn = bool(__fastcall*)(void* thiz, void* str, void* str2, void* method);
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;
static IsUnlockedFn1 oHasPerk = nullptr;
static IsUnlockedFn oHasOutfit = nullptr;
static UpdateLockedFn oUpdPets = nullptr;
static UpdateLockedFn oUpdPerks = nullptr;
static UpdateLockedFn oUpdFlash = nullptr;
static UpdateLockedFn oUpdEmotes = nullptr;
static UpdateLockedFn oUpdOutfits = nullptr;
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;
// Forward for UpdateLocked hooks — we patch them to bypass locked UI
static bool hookUpdateLocked(void* addr, const char* name, bool shouldUnlock, void (__fastcall *hook)(void*, void*));
static std::string il2cppStringToUtf8(void* ilStr) {
if (!ilStr) return "";
// Il2CppString layout: length at offset 0x10, chars at 0x14
int32_t len = *reinterpret_cast<int32_t*>(reinterpret_cast<uint8_t*>(ilStr) + 0x10);
wchar_t* chars = reinterpret_cast<wchar_t*>(reinterpret_cast<uint8_t*>(ilStr) + 0x14);
if (len <= 0 || len > 512) return "";
char buf[1024]{};
int n = WideCharToMultiByte(CP_UTF8, 0, chars, len, buf, sizeof(buf)-1, nullptr, nullptr);
if (n > 0) return std::string(buf, n);
return "";
}
static bool __fastcall hIsRobe(void* thiz, void* robeStr, void* characterStr, void* method) { return true; }
static bool __fastcall hIsChar(void* thiz, void* charStr, void* method) {
logPatch((std::string("[trace] IsCharacterUnlocked(\"") + il2cppStringToUtf8(charStr) + "\") -> true").c_str());
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
// (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 hWaitForPurchasedPet(void* thiz, void* method) { if (oWaitPet) oWaitPet(thiz, method); }
static void __fastcall hUpdateLockedPerks(void* thiz, void* method) { if (oUpdPerks) oUpdPerks(thiz, method); }
static void __fastcall hUpdateLockedFlashlights(void* thiz, void* method) { if (oUpdFlash) oUpdFlash(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 hUpdateLockedRobes(void* thiz, void* method) { if (oUpdRobes) oUpdRobes(thiz, method); }
// 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) {
// pass-through + trace (v2.1.0 proved forcing true blocks purchases)
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) {
// MUST stay pass-through: forcing true desyncs lobby validation vs injected
// userItems and UNLOCKS REGRESS (pets locked again). See v2.1.1 regression.
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;
}
// 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; }
(void)shouldUnlock; // always-on
void* orig = nullptr;
MH_STATUS st = MH_CreateHook(addr, (void*)hook, &orig);
if (st == MH_OK) {
MH_EnableHook(addr);
if (origOut) *origOut = reinterpret_cast<UpdateLockedFn>(orig);
logPatch((std::string("[DevourUnlocker] Hooked ")+name).c_str());
return true;
}
logPatch((std::string("[DevourUnlocker] Hook ")+name+" failed st="+std::to_string((int)st)).c_str());
return false;
}
bool installPatches() {
logPatch("[DevourUnlocker] installPatches enter");
if (g_installed) { logPatch("[DevourUnlocker] already installed"); return true; }
if (!g_mhInit) {
MH_STATUS st = MH_Initialize();
if (st != MH_OK && st != MH_ERROR_ALREADY_INITIALIZED) {
std::cerr << "[DevourUnlocker] MH_Initialize failed " << st << "\n";
logPatch(("[DevourUnlocker] MH_Initialize failed "+std::to_string(st)).c_str());
return false;
}
g_mhInit = true;
logPatch("[DevourUnlocker] MH_Initialize ok (or already)");
}
if (!Il2CppResolver::init()) {
std::cerr << "[DevourUnlocker] GameAssembly not found\n";
logPatch("[DevourUnlocker] GameAssembly not found");
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();
auto hasOutfit = Il2CppResolver::hasOutfit();
auto updPets = Il2CppResolver::updateLockedPets();
auto updPerks = Il2CppResolver::updateLockedPerks();
auto updFlash = Il2CppResolver::updateLockedFlashlights();
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 << " 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);
{
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);
}
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) {
MH_EnableHook(ch.address);
logPatch("[DevourUnlocker] Hooked IsCharacterUnlocked");
any = true;
} else logPatch("[DevourUnlocker] Hook IsChar failed");
}
if (hasPerk.valid()) {
if (MH_CreateHook(hasPerk.address, (void*)hHasPerk, (void**)&oHasPerk) == MH_OK) {
MH_EnableHook(hasPerk.address);
logPatch("[DevourUnlocker] Hooked HasPerk");
any = true;
} else logPatch("[DevourUnlocker] Hook HasPerk failed");
}
if (hasOutfit.valid()) {
if (MH_CreateHook(hasOutfit.address, (void*)hHasOutfit, (void**)&oHasOutfit) == MH_OK) {
MH_EnableHook(hasOutfit.address);
logPatch("[DevourUnlocker] Hooked HasOutfit");
any = true;
} else logPatch("[DevourUnlocker] Hook HasOutfit failed");
}
// Ownership + currency overrides — the actual gates for pets/perks/flashlights/emotes/outfits
{
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 PlayerHasItem=%p HasRetrievedUserItems=%p", uhi.address, tok.address, phi.address, hri.address);
logPatch(b2);
if (uhi.valid()) {
void* orig = nullptr;
if (MH_CreateHook(uhi.address, (void*)hUserHasItem, &orig) == MH_OK) {
MH_EnableHook(uhi.address);
oUserHasItem = reinterpret_cast<UserHasItemFn>(orig);
logPatch("[DevourUnlocker] Hooked UserHasItem -> always owned");
any = true;
} else logPatch("[DevourUnlocker] Hook UserHasItem failed");
} else logPatch("[DevourUnlocker] UserHasItem not found");
if (tok.valid()) {
void* orig = nullptr;
if (MH_CreateHook(tok.address, (void*)hGetCurrentTokens, &orig) == MH_OK) {
MH_EnableHook(tok.address);
oGetTokens = reinterpret_cast<GetTokensFn>(orig);
logPatch("[DevourUnlocker] Hooked GetCurrentTokens -> 9999999");
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);
any |= hookUpdateLocked(updFlash.address, "UpdateLockedFlashlights", Config::get().unlockFlashlights, hUpdateLockedFlashlights, &oUpdFlash);
any |= hookUpdateLocked(updEmotes.address, "UpdateLockedEmotes", Config::get().unlockEmotes, hUpdateLockedEmotes, &oUpdEmotes);
any |= hookUpdateLocked(updOutfits.address, "UpdateLockedOutfits", Config::get().unlockOutfits, hUpdateLockedOutfits, &oUpdOutfits);
any |= hookUpdateLocked(updRobes.address, "UpdateLockedRobes", Config::get().unlockRobes, hUpdateLockedRobes, &oUpdRobes);
any |= hookUpdateLocked(waitPet.address, "WaitForPurchasedPetValidation", Config::get().unlockPets, hWaitForPurchasedPet, &oWaitPet);
// 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 hooks not found — scheduling retry for 60s (IL2CPP may not be ready until lobby)");
CreateThread(nullptr, 0, [](LPVOID)->DWORD{
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();
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"); } }
if(hp.valid()){ if(MH_CreateHook(hp.address,(void*)hHasPerk,(void**)&oHasPerk)==MH_OK){ MH_EnableHook(hp.address); any2=true; logPatch("[DevourUnlocker] Retry hooked HasPerk"); } }
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);
if(p4.valid()) any2 |= hookUpdateLocked(p4.address, "UpdateLockedEmotes[retry]", Config::get().unlockEmotes, hUpdateLockedEmotes, &oUpdEmotes);
if(p5.valid()) any2 |= hookUpdateLocked(p5.address, "UpdateLockedOutfits[retry]", Config::get().unlockOutfits, hUpdateLockedOutfits, &oUpdOutfits);
if(p6.valid()) any2 |= hookUpdateLocked(p6.address, "UpdateLockedRobes[retry]", Config::get().unlockRobes, hUpdateLockedRobes, &oUpdRobes);
if(pw.valid()) any2 |= hookUpdateLocked(pw.address, "WaitForPurchasedPetValidation[retry]", Config::get().unlockPets, hWaitForPurchasedPet, &oWaitPet);
if(any2){ logPatch("[DevourUnlocker] Retry hooked missing targets — done"); 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);
}
g_installed = any;
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;
}
void removePatches() {
if (!g_installed) return;
MH_DisableHook(MH_ALL_HOOKS);
MH_Uninitialize();
g_mhInit = false;
g_installed = false;
}
bool patchesInstalled() { return g_installed; }
#else // Linux stub — uses funchook or LD_PRELOAD interpose if needed
bool installPatches() {
std::cout << "[DevourUnlocker] Linux patches stub — use winhttp.dll under Proton\n";
return false;
}
void removePatches() {}
bool patchesInstalled() { return false; }
#endif
} // namespace devour