#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include "zygisk.hpp"
#include "overlay_dex.h"
#include <jni.h>
#include <dlfcn.h>
#include <sys/mman.h>
#include <unistd.h>
#include <string.h>
#include <android/log.h>
#include <sys/system_properties.h>
#include <pthread.h>
#include <time.h>
#include <errno.h>
#include <stdlib.h>
#include <stdarg.h>
#include <sys/syscall.h>
#include <linux/memfd.h>
#include <string>
#include "lsplant.hpp"
#include <zlib.h>

#define TAG "DRMod"
#define LOGI(...) do { __android_log_print(ANDROID_LOG_INFO, TAG, __VA_ARGS__); drmod_log(__VA_ARGS__); } while(0)
#define LOGE(...) do { __android_log_print(ANDROID_LOG_ERROR, TAG, __VA_ARGS__); drmod_log(__VA_ARGS__); } while(0)

static bool g_is_game = false;

static FILE *g_logfile = nullptr;
static void drmod_log(const char *fmt, ...) {
    if (!g_logfile) {
        // Only target process logs to file (SELinux-safe app dir). Other processes log only to logcat.
        if (!g_is_game) return;
        mkdir("/data/data/com.lavalabs.dungeonrush/files", 0755);
        mkdir("/data/user/0/com.lavalabs.dungeonrush/files", 0755);
        g_logfile = fopen("/data/data/com.lavalabs.dungeonrush/files/drmod.log", "a");
        if (!g_logfile) g_logfile = fopen("/data/user/0/com.lavalabs.dungeonrush/files/drmod.log", "a");
    }
    if (!g_logfile) return;
    va_list ap;
    va_start(ap, fmt);
    time_t t = time(nullptr);
    struct tm *tm = localtime(&t);
    fprintf(g_logfile, "[%02d:%02d:%02d] [pid=%d] ", tm->tm_hour, tm->tm_min, tm->tm_sec, getpid());
    vfprintf(g_logfile, fmt, ap);
    fprintf(g_logfile, "\n");
    va_end(ap);
    fflush(g_logfile);
}

#define GAME_PKG "com.lavalabs.dungeonrush"
#define GAME_PKG_FULL "com.lavalabs.dungeonrush"
#define DELAY_SEC 30

static JavaVM *g_jvm = nullptr;
static bool g_activated = false;

// ==================== Toggle state (ALL OFF by default) ====================

static volatile bool g_immortality   = false;
static volatile bool g_forced_crits  = false;
static volatile bool g_minigame_loot = false;
static volatile bool g_divine_forge  = false;
static volatile bool g_overdrive_divine = false;
static volatile bool g_forge_exp     = false;

// ==================== Shared state ====================

static uintptr_t g_il2cpp_base = 0;

// ==================== Patch table ====================

struct Patch {
    uint32_t off;
    uint32_t len;
    const uint8_t *orig;
    const uint8_t *repl;
    const char *name;
    bool toggleable;
};

static const uint8_t NOP4[]  = {0x1F,0x20,0x03,0xD5};
static const uint8_t NOP8[]  = {0x1F,0x20,0x03,0xD5,0x1F,0x20,0x03,0xD5};
static const uint8_t MOV_W0_1[] = {0x20,0x00,0x80,0x52};
static const uint8_t MOV_W0_0[] = {0x00,0x00,0x80,0x52};
static const uint8_t MOV_X0_0[] = {0x00,0x00,0x80,0xD2};
static const uint8_t MOV_W0_9[] = {0x20,0x01,0x80,0x52};
static const uint8_t RET[]       = {0xC0,0x03,0x5F,0xD6};
static const uint8_t MR_RET[]    = {0x20,0x00,0x80,0x52,0xC0,0x03,0x5F,0xD6};
static const uint8_t M9_RET[]    = {0x20,0x01,0x80,0x52,0xC0,0x03,0x5F,0xD6};
static const uint8_t M2_RET[]    = {0x40,0x00,0x80,0x52,0xC0,0x03,0x5F,0xD6};
static const uint8_t FMOV_S0[]   = {0xE0,0x03,0x27,0x1E};

// Original bytes (verified against v56 binary)
static const uint8_t O_ctz[]     = {0xFE,0x57,0xBE,0xA9,0xF4,0x4F,0x01,0xA9}; // stp x30,x21 / stp x20,x19
static const uint8_t O_store1[]  = {0x41,0x16,0x00,0x54}; // b.ne HTTP 200 check in fh.dyd (TrackingController.fh)
static const uint8_t O_store2[]  = {0x41,0x1A,0x00,0x54}; // b.ne HTTP 200 check in fi.dye (TrackingController.fi)
static const uint8_t O_tbz_fc[]  = {0x55,0x1C,0x00,0x36}; // tbz w21,#0 -> fi.dye entry flag guard (bit0==0 => FAILURE)
static const uint8_t O_actk_bl[]  = {0xC3,0xFF,0xFF,0x97};
static const uint8_t O_actk_cs[]  = {0xE0,0x17,0x9F,0x1A};
static const uint8_t O_actk_bl2[] = {0xC1,0xFF,0xFF,0x97};
static const uint8_t O_actk_bl3[] = {0xBF,0xFF,0xFF,0x97};
static const uint8_t O_mg[]       = {0x00,0x28,0x23,0x1E};
static const uint8_t O_forge[]    = {0xE9,0x23,0xBC,0x6D,0xFE,0x0B,0x00,0xF9};
static const uint8_t O_od[]       = {0xFF,0x43,0x02,0xD1,0xFE,0x6F,0x04,0xA9};
static const uint8_t O_beq_38[]   = {0x00,0x07,0x00,0x54}; // b.eq +0xE0
static const uint8_t B_SYNC_38[]  = {0x38,0x00,0x00,0x14}; // b +0xE0 (same target as b.eq)
static const uint8_t O_jme_gate[] = {0x1A,0x02,0x00,0x36}; // tbz w26,#0 (Exceptional gate) in tw.jme @0x28741C8
static const uint8_t O_jmg[]      = {0xE8,0x0F,0x1E,0xFC,0xFE,0x07,0x00,0xF9}; // tw.jmg prologue @0x287478C

static const Patch patches[] = {
    // Ad Skip v56 (always on) - czj=IsRewardedAdReady, czk=IsInterstitialReady (AdvertisementController)
    {0x27C07A8, 8, O_ctz,     MR_RET, "czj IsRewardedAdReady v56", false},
    {0x27C0830, 8, O_ctz,     MR_RET, "czk IsInterstitialReady v56", false},
    // Store Bypass native v56 (always on) - fh.dyd/fi.dye HTTP 200 check
    {0x2A7E234, 4, O_store1,  NOP4,   "Store bypass #1 fh.dyd v56",  false},
    {0x2A7E6C8, 4, O_store2,  NOP4,   "Store bypass #2 fi.dye v56",  false},
    // fi.dye entry flag guard: tbz w21,#0 (bit0) -> force SUCCESS path (v56)
    {0x2A7E688, 4, O_tbz_fc,  NOP4,   "fi.dye entry tbz bit0 v56",  false},
    // ACTk Disable v56 (always on)
    {0x27BC304, 4, O_actk_bl,  MOV_W0_0, "ACTk Int.set v56",     false},
    {0x27BC3F8, 4, O_actk_cs,  MOV_W0_1, "ACTk Int.get v56",     false},
    {0x27BC744, 4, O_actk_bl2, MOV_X0_0, "ACTk Long.set v56",    false},
    {0x27BC848, 4, O_actk_cs,  MOV_W0_1, "ACTk Long.get v56",    false},
    {0x27BBE84, 4, O_actk_bl3, MOV_W0_0, "ACTk Float.set v56",   false},
    {0x27BBF88, 4, O_actk_cs,  MOV_W0_1, "ACTk Float.get v56",   false},
    // Minigame Loot - Destroy 0% wv.lhu (toggleable)
    {0x28E9198, 4, O_mg,     FMOV_S0, "Minigame Destroy 0% wv.lhu v56", true},
    // Divine Forge tw.jml + Overdrive wv.lhy (toggleable)
    {0x2870270, 8, O_forge,  M9_RET,  "Forge 100% Divine tw.jml v56",   true},
    {0x28E7E28, 8, O_od,     M9_RET,  "Overdrive 100% Divine wv.lhy v56", true},
    // Forge Exp Substats (toggleable) - NOP gate Exp in tw.jme + jmg max 2
    {0x28701C8, 4, O_jme_gate, NOP4,  "Forge Exp gate tw.jme v56",  true},
    {0x287078C, 8, O_jmg,     M2_RET, "Forge Exp max tw.jmg v56",   true},
    // ACTk sync-restore disable v56 (b.eq +0xE0 -> b +0xE0 at entry+0x88, keep field value)
    {0x27BE3DC, 4, O_beq_38, B_SYNC_38, "ACTk sync #1 ct.cwz", false},
    {0x27BEA00, 4, O_beq_38, B_SYNC_38, "ACTk sync #2 ct.cxf", false},
    {0x2A81C50, 4, O_beq_38, B_SYNC_38, "ACTk sync #3 User.eyt", false},
    {0x2B72BA0, 4, O_beq_38, B_SYNC_38, "ACTk sync #4 CapeModel.fph", false},
    {0x2810CDC, 4, O_beq_38, B_SYNC_38, "ACTk sync #5 CompanionModel.htw", false},
    {0x286FA64, 4, O_beq_38, B_SYNC_38, "ACTk sync #6 EquippedItemModel.jly", false},
    {0x28BA124, 4, O_beq_38, B_SYNC_38, "ACTk sync #7 MasteryUpgradeModel.khg", false},
    {0x28C75F0, 4, O_beq_38, B_SYNC_38, "ACTk sync #8 MiningData.kps", false},
    {0x297E428, 4, O_beq_38, B_SYNC_38, "ACTk sync #9 WingModel.oad", false},
};
static const int num_patches = sizeof(patches) / sizeof(patches[0]);

// ==================== Helpers ====================

static long page_size() { return sysconf(_SC_PAGESIZE); }

static int get_page_prot(uintptr_t addr) {
    long ps = page_size();
    uintptr_t pg = addr & ~(ps - 1);
    FILE *f = fopen("/proc/self/maps", "r");
    if (!f) return PROT_READ;
    char line[512];
    int prot = 0;
    while (fgets(line, sizeof(line), f)) {
        unsigned long start, end;
        char perms[5];
        if (sscanf(line, "%lx-%lx %4s", &start, &end, perms) == 3) {
            if (pg >= start && pg < end) {
                if (perms[0] == 'r') prot |= PROT_READ;
                if (perms[1] == 'w') prot |= PROT_WRITE;
                if (perms[2] == 'x') prot |= PROT_EXEC;
                break;
            }
        }
    }
    fclose(f);
    return prot ? prot : PROT_READ;
}

static int set_prot(uintptr_t addr, size_t len, int prot) {
    long ps = page_size();
    uintptr_t pg = addr & ~(ps - 1);
    size_t full = (addr + len + ps - 1) & ~(ps - 1);
    return mprotect((void *)pg, full - pg, prot);
}

static bool proc_mem_write(uintptr_t addr, const void *data, size_t len) {
    // Fast path: direct mprotect + memcpy for own process (atomic, no su, no race)
    long ps = page_size();
    uintptr_t pg = addr & ~(ps - 1);
    size_t full = (addr + len + ps - 1) & ~(ps - 1);
    size_t sz = full - pg;
    int oldProt = get_page_prot(addr);
    if (set_prot(addr, len, PROT_READ | PROT_WRITE | PROT_EXEC) == 0) {
        memcpy((void*)addr, data, len);
        __builtin___clear_cache((char*)addr, (char*)addr + len);
        int restore = PROT_READ | PROT_EXEC;
        if (oldProt & PROT_WRITE) restore |= PROT_WRITE;
        mprotect((void*)pg, sz, restore);
        if (memcmp((void*)addr, data, len) == 0) {
            LOGI("proc_mem_write: direct OK addr=0x%lx len=%zu", (unsigned long)addr, len);
            return true;
        }
        LOGI("proc_mem_write: direct verify fail, falling back to su dd");
    } else {
        LOGI("proc_mem_write: direct mprotect failed, falling back to su dd");
    }
    // Fallback to su dd (original logic) for cases where direct fails (SELinux)
    static int s_counter = 0;
    char tmpfile[256];
    snprintf(tmpfile, sizeof(tmpfile), "/data/data/com.lavalabs.dungeonrush/files/drmod_pw_%d", s_counter++);
    int tf = open(tmpfile, O_CREAT | O_WRONLY | O_TRUNC, 0600);
    if (tf < 0) {
        LOGE("proc_mem_write: open tmp failed errno=%d (%s) path=%s", errno, strerror(errno), tmpfile);
        return false;
    }
    ssize_t w = write(tf, data, len);
    close(tf);
    if (w < 0 || (size_t)w != len) {
        LOGE("proc_mem_write: write to tmp failed errno=%d (%s) path=%s", errno, strerror(errno), tmpfile);
        unlink(tmpfile);
        return false;
    }
    int pid = getpid();
    char cmd[512];
    snprintf(cmd, sizeof(cmd), "su -c 'dd if=%s of=/proc/%d/mem bs=1 seek=0x%lx count=%zu conv=notrunc 2>/dev/null; rc=$?; rm -f %s; exit $rc'",
             tmpfile, pid, (unsigned long)addr, len, tmpfile);
    LOGI("proc_mem_write: su dd addr=0x%lx len=%zu pid=%d tmp=%s", (unsigned long)addr, len, pid, tmpfile);
    int rc = system(cmd);
    if (rc != 0) {
        LOGE("proc_mem_write: su dd FAILED rc=%d addr=0x%lx tmp=%s", rc, (unsigned long)addr, tmpfile);
        return false;
    }
    LOGI("proc_mem_write: su dd OK addr=0x%lx len=%zu", (unsigned long)addr, len);
    return true;
}

static bool patch_bytes(uintptr_t base, const Patch &p) {
    uintptr_t a = base + p.off;
    if (p.orig && memcmp((void *)a, p.orig, p.len) != 0) {
        LOGE("VERIFY FAIL (pre-write): %s @0x%X", p.name, p.off);
        return false;
    }
    if (!proc_mem_write(a, p.repl, p.len)) {
        LOGE("proc_mem_write FAIL: %s @0x%X", p.name, p.off);
        return false;
    }
    LOGI("  proc_mem_write OK: %s @0x%X len=%u", p.name, p.off, p.len);
    __builtin___clear_cache((char *)a, (char *)a + p.len);
    LOGI("  cache clear OK: %s", p.name);
    if (memcmp((void *)a, p.repl, p.len) != 0) {
        LOGE("VERIFY FAIL (post-write): %s @0x%X", p.name, p.off);
        return false;
    }
    LOGI("OK: %s @0x%X", p.name, p.off);
    return true;
}

static bool unpatch_bytes(uintptr_t base, const Patch &p) {
    if (!p.toggleable || !p.orig) return true;
    uintptr_t a = base + p.off;
    LOGI("unpatch: %s @0x%X len=%u", p.name, p.off, p.len);
    if (!proc_mem_write(a, p.orig, p.len)) {
        LOGE("unpatch proc_mem_write FAIL: %s @0x%X", p.name, p.off);
        return false;
    }
    __builtin___clear_cache((char *)a, (char *)a + p.len);
    if (memcmp((void *)a, p.orig, p.len) != 0) {
        LOGE("REVERT VERIFY FAIL: %s @0x%X", p.name, p.off);
        return false;
    }
    LOGI("REVERTED: %s @0x%X", p.name, p.off);
    return true;
}

// ==================== Mod Menu ====================

static jclass    g_overlay_cls = nullptr;
static jobject   g_app_context = nullptr;
static jmethodID g_native_toggle_mid = nullptr;
static jobject   g_dex_loader = nullptr;

static int find_patch(const char *substr) {
    for (int i = 0; i < num_patches; i++)
        if (strstr(patches[i].name, substr)) return i;
    return -1;
}

static int fetch_patch(int idx, const char *substr, const char *what) {
    if (idx >= 0) return idx;
    idx = find_patch(substr);
    LOGE("toggle: patch '%s' hardcoded idx invalid, fallback lookup=%d", what, idx);
    return idx;
}

static void native_toggle_cb(JNIEnv *env, jclass, jint feature, jboolean enabled) {
    bool on = enabled;
    LOGI("Toggle feature=%d on=%d", feature, on);
    switch (feature) {
        case 0: g_immortality = on;   break;
        case 1: g_forced_crits = on;  break;
        case 2: { // Divine Forge - tn.jky 100% Divine
            g_divine_forge = on;
            static int idx = -1;
            idx = fetch_patch(idx, "Forge 100% Divine", "forge");
            if (idx < 0) { LOGE("toggle forge: patch not found"); break; }
            if (g_il2cpp_base) {
                const Patch &p = patches[idx];
                if (on) { bool r = patch_bytes(g_il2cpp_base, p); LOGI("toggle forge patch result=%d", r); }
                else    { bool r = unpatch_bytes(g_il2cpp_base, p); LOGI("toggle forge unpatch result=%d", r); }
            } else { LOGI("toggle forge: g_il2cpp_base=0, deferred"); }
            break;
        }
        case 3: { // Divine Overdrive - wn.lgl 100% Divine
            g_overdrive_divine = on;
            static int idx = -1;
            idx = fetch_patch(idx, "Overdrive 100% Divine", "overdrive");
            if (idx < 0) { LOGE("toggle overdrive: patch not found"); break; }
            if (g_il2cpp_base) {
                const Patch &p = patches[idx];
                if (on) { bool r = patch_bytes(g_il2cpp_base, p); LOGI("toggle overdrive patch result=%d", r); }
                else    { bool r = unpatch_bytes(g_il2cpp_base, p); LOGI("toggle overdrive unpatch result=%d", r); }
            } else { LOGI("toggle overdrive: g_il2cpp_base=0, deferred"); }
            break;
        }
        case 4: { // Minigame Loot - wn.lgh Destroy 0%
            g_minigame_loot = on;
            static int idx = -1;
            idx = fetch_patch(idx, "Minigame Destroy", "minigame");
            if (idx < 0) { LOGE("toggle minigame: patch not found"); break; }
            if (g_il2cpp_base) {
                const Patch &p = patches[idx];
                if (on) { bool r = patch_bytes(g_il2cpp_base, p); LOGI("toggle minigame patch result=%d", r); }
                else    { bool r = unpatch_bytes(g_il2cpp_base, p); LOGI("toggle minigame unpatch result=%d", r); }
            } else { LOGI("toggle minigame: g_il2cpp_base=0, deferred"); }
            break;
        }
        case 5: { // Forge Exp Substats - gate+max in tw (separate from Divine Forge)
            g_forge_exp = on;
            static int idx_g = -1, idx_m = -1;
            idx_g = fetch_patch(idx_g, "Forge Exp gate", "forge_exp_gate");
            idx_m = fetch_patch(idx_m, "Forge Exp max", "forge_exp_max");
            if (idx_g < 0 || idx_m < 0) { LOGE("toggle forge_exp: patch not found"); break; }
            if (g_il2cpp_base) {
                const Patch &pg = patches[idx_g];
                const Patch &pm = patches[idx_m];
                bool r1 = false, r2 = false;
                if (on) { r1 = patch_bytes(g_il2cpp_base, pg); r2 = patch_bytes(g_il2cpp_base, pm); }
                else    { r1 = unpatch_bytes(g_il2cpp_base, pg); r2 = unpatch_bytes(g_il2cpp_base, pm); }
                LOGI("toggle forge_exp patch result=%d/%d", r1, r2);
            } else { LOGI("toggle forge_exp: g_il2cpp_base=0, deferred"); }
            break;
        }
    }
}

static void load_dex(JNIEnv *env) {
    if (g_dex_loader) return;
    LOGI("load_dex: start (dex size=%d)", overlay_dex_size);

    void *dex_buf = (void *)overlay_dex;
    jint dex_len = overlay_dex_size;

    jclass llc = env->FindClass("java/lang/ClassLoader");
    if (!llc) { env->ExceptionClear(); LOGE("load_dex: ClassLoader not found"); return; }
    jmethodID llc_getDef = env->GetStaticMethodID(llc, "getSystemClassLoader", "()Ljava/lang/ClassLoader;");
    jobject base_loader = env->CallStaticObjectMethod(llc, llc_getDef);
    if (!base_loader) { env->ExceptionClear(); LOGE("load_dex: getSystemClassLoader failed"); return; }
    LOGI("load_dex: system classloader obtained");

    jclass idclc = env->FindClass("dalvik/system/InMemoryDexClassLoader");
    if (!idclc) { env->ExceptionClear(); LOGE("load_dex: InMemoryDexClassLoader not found"); return; }
    jmethodID idclc_init = env->GetMethodID(idclc, "<init>",
        "(Ljava/nio/ByteBuffer;Ljava/lang/ClassLoader;)V");
    jobject buf = env->NewDirectByteBuffer(dex_buf, dex_len);
    if (!buf) { env->ExceptionClear(); LOGE("load_dex: NewDirectByteBuffer failed"); return; }
    jobject loader = env->NewObject(idclc, idclc_init, buf, base_loader);
    if (env->ExceptionCheck()) { env->ExceptionClear(); LOGE("DEX load failed"); return; }
    LOGI("load_dex: InMemoryDexClassLoader created");

    g_dex_loader = env->NewGlobalRef(loader);
    env->DeleteLocalRef(loader);
    env->DeleteLocalRef(buf);
    env->DeleteLocalRef(base_loader);
    LOGI("DEX loaded");
}

static jclass load_class(JNIEnv *env, const char *name) {
    if (!g_dex_loader) { LOGE("load_class: no dex loader for %s", name); return nullptr; }
    jclass llc = env->FindClass("java/lang/ClassLoader");
    if (!llc) { env->ExceptionClear(); LOGE("load_class: ClassLoader not found for %s", name); return nullptr; }
    jmethodID loadCls = env->GetMethodID(llc, "loadClass", "(Ljava/lang/String;)Ljava/lang/Class;");
    if (!loadCls) { env->ExceptionClear(); LOGE("load_class: loadClass method not found for %s", name); env->DeleteLocalRef(llc); return nullptr; }
    jstring jname = env->NewStringUTF(name);
    if (!jname) { env->ExceptionClear(); LOGE("load_class: NewStringUTF failed for %s", name); env->DeleteLocalRef(llc); return nullptr; }
    jclass cls = (jclass)env->CallObjectMethod(g_dex_loader, loadCls, jname);
    bool ok = (cls != nullptr);
    if (env->ExceptionCheck()) { env->ExceptionClear(); ok = false; }
    env->DeleteLocalRef(jname);
    env->DeleteLocalRef(llc);
    if (!ok) { LOGE("load_class: %s NOT found", name); return nullptr; }
    LOGI("load_class: %s found", name);
    return cls;
}

static void create_overlay(JNIEnv *env) {
    if (!g_app_context) { LOGE("create_overlay: no app context"); return; }
    LOGI("create_overlay: start");

    load_dex(env);
    if (!g_dex_loader) { LOGE("create_overlay: no dex loader"); return; }

    jclass cls = load_class(env, "com/drmod/Overlay");
    if (!cls) { LOGE("Overlay class not found"); return; }

    JNINativeMethod methods[] = {
        {(char *)"nativeToggle", (char *)"(IZ)V", (void *)native_toggle_cb}
    };
    if (env->RegisterNatives(cls, methods, 1) != 0) {
        env->ExceptionClear();
        LOGE("create_overlay: RegisterNatives failed");
    } else {
        LOGI("create_overlay: nativeToggle registered");
    }

    jmethodID init_mid = env->GetStaticMethodID(cls, "init", "(Landroid/content/Context;)V");
    if (!init_mid) { env->ExceptionClear(); LOGE("Overlay.init not found"); return; }
    LOGI("create_overlay: Overlay.init resolved, calling...");

    env->CallStaticVoidMethod(cls, init_mid, g_app_context);
    if (env->ExceptionCheck()) {
        env->ExceptionDescribe();
        env->ExceptionClear();
        LOGE("Overlay.init failed");
    } else {
        LOGI("Overlay created");
    }
    env->DeleteLocalRef(cls);
}

// ==================== Immortality + Forced Crits ====================

#define VTABLE_BASE 0x150  // v54: GridCombatController 0x150 (v51: 0x138)
#define SLOT_SZ     16
#define FOP_SLOT    21
#define FOP_OFF     (VTABLE_BASE + FOP_SLOT * SLOT_SZ)
#define OFF_ISMAIN  0x30C  // v54: Soldier.IsMainCharacter 0x30C (v51: 0x2E4)
#define OFF_CRITDMG 0xC8  // unchanged (Character.CriticalDamage 0xC8)

typedef void (*fop_fn)(void *self, void *attacker, double dmg, int critTier, bool flag);
static fop_fn g_orig_fop = nullptr;
static void  *g_main     = nullptr;

static inline bool is_pointer_valid(void *p) {
    if (!p) return false;
    uintptr_t v = (uintptr_t)p;
    if (v < 0x10000) return false;
    // Scudo poison
    if (v == 0x0101010101010000ULL || v == 0x0202020202020000ULL || v == 0x0303030303030000ULL) return false;
    if ((v & 0x00FFFFFFFFFFFFFFULL) == 0x001010101010000ULL) return false;
    // Verify page is mapped via msync (lightweight, no fopen)
    uintptr_t page = v & ~0xFFFULL;
    if (msync((void*)page, 4096, MS_ASYNC) != 0) {
        if (errno == ENOMEM) return false;
    }
    return true;
}

static int g_fop_call_count = 0;
static void my_fop(void *self, void *attacker, double dmg, int critTier, bool flag) {
    // ===== PATCH 1: IMMORTALITY (Character.fwa, self must be main) =====
    bool is_main_self = false;
    if (is_pointer_valid(self)) {
        is_main_self = *(bool *)((uint8_t *)self + OFF_ISMAIN);
        if (is_main_self) {
            g_main = self; // cache only valid main
            if (g_fop_call_count < 5)
                LOGI("FOP_IMM: MAIN hit (immortality=%d, self=%p attacker=%p dmg=%.1f tier=%d flag=%d)",
                     g_immortality, self, attacker, dmg, critTier, flag);
            g_fop_call_count++;
            if (g_immortality) {
                if (g_fop_call_count < 10)
                    LOGI("FOP_IMM: ABSORBED — immortality ON, skipping original (self=%p dmg=%.1f)", self, dmg);
                return;
            }
        }
    } else {
        if (g_fop_call_count < 20) LOGI("FOP_IMM: skip invalid self=%p", self);
    }
    // ===== PATCH 2: FORCED CRIT (v4.80: fwa chiamato su self=RICEVENTE, attacker=INFLIGGITORE) =====
    // Firma reale: Character.fwa(Character a, double b, CritTier c, bool d) -> viene chiamata
    // SULL'OBIETTIVO (this=self subisce il danno) con a=attacker (chi infligge). v4.78/4.79
    // applicava il crit quando self era il main -> amplificava il danno SUBITO dal player =
    // i crit li facevano gli AVVERSARI al giocatore (patch invertita). FIX: il crit scatta
    // quando l'INFLIGGITORE (attacker) e' il main; CriticalDamage va letta dall'attacker.
    if (g_forced_crits && is_pointer_valid(attacker)) {
        bool attacker_is_main = *(bool *)((uint8_t *)attacker + OFF_ISMAIN);
        if (attacker_is_main && self != attacker) {
            float cd = *(float *)((uint8_t *)attacker + OFF_CRITDMG);
            if (cd > 0.5f && cd < 100.0f) {
                double dmg_n = dmg * (double)cd;
                if (g_fop_call_count < 15)
                    LOGI("FOP_CRIT: FORCED CRIT (dmg=%.1f -> %.1f, cd=%.1f, self=%p attacker=main=%p)",
                         dmg, dmg_n, cd, self, attacker);
                dmg = dmg_n;
                critTier = 2;
            } else {
                if (g_fop_call_count < 15)
                    LOGI("FOP_CRIT: crit req but CriticalDamage invalid attacker=%p cd=%.1f", attacker, cd);
            }
        }
    }
    // Fallback to original - ensure g_orig_fop is valid
    if (g_orig_fop) g_orig_fop(self, attacker, dmg, critTier, flag);
}

// ==================== User.eyu hook (dungeon keys A/B/C) ====================
// eyu(ref ObscuredInt a, ref int b, int c) = User private static PRIMITIVE SETTER @RVA 0x2A85D40.
// OGNI property int di User passa da qui (x0=hidden this+hidden_off, x1=field this+field_off,
// w2=value). L'originale scrive il campo in PLAINTEXT: `str w19,[x20]` con x20=x1 => *field = value.
// Quindi: field_off = b - g_user_base; old = *(int*)b; e passiamo all'originale un valore modificato.
static void *inline_hooker(void *target, void *hooker_fn);   // forward decl (defined later)
// g_code_base DEVE essere definito prima di ogni uso (install_user_eyu_probe lo usa a RVA).
// Convenzione: addr_assoluto = g_code_base + RVA (impostata in install_store_hook come
// g_proc_purchase_entry - 0x28ACD34). Tutti i probe devono usare g_code_base, NON g_il2cpp_base.
static uintptr_t g_code_base = 0;
typedef void (*user_eyu_fn)(void *a, void *b, int c);
static user_eyu_fn g_orig_user_eyu = nullptr;
static volatile int g_user_eyu_count = 0;
#define USER_EYU_MAX_LOG 8000

// Campi dungeon keys (dump.cs User, VERIFICATI):
//   DragonBoss:   Daily 0x140, Bonus 0x144, LastReset 0x148, Watch 0x150
//   ZombieHorde:  Daily 0x1C0, Bonus 0x1C4, LastReset 0x1C8, Watch 0x1CC
//   Cultist:      Daily 0x1D8, Bonus 0x1DC, LastReset 0x1E0, Watch 0x1E4
//   Exceptional:  Daily 0x1F0, Bonus 0x1F4, LastReset 0x1F8, Watch 0x1FC
struct DungeonFields { uint32_t daily, bonus, reset, watch; };
static const DungeonFields DUNGEON_FIELDS[4] = {
    {0x140, 0x144, 0x148, 0x150},
    {0x1C0, 0x1C4, 0x1C8, 0x1CC},
    {0x1D8, 0x1DC, 0x1E0, 0x1E4},
    {0x1F0, 0x1F4, 0x1F8, 0x1FC},
};
// g_user_base appreso dal flush hook della store chain: u = *(x0+0x28) (st_chain_entry_flush).
static uintptr_t g_user_base = 0;
#define KEY_BONUS_MULT 5              // B: ogni rewarded ad concede KEY_BONUS_MULT chiavi bonus
#define KEY_RESET_WINDOW_MS 6000      // A: finestra reset aperta dal write di Last*KeyResetTime
static volatile int64_t g_reset_win_until[4] = {0,0,0,0}; // per-dungeon reset window (ms monotonic)
static volatile int64_t g_last_ad_reward_ms = 0;          // B: settata nei case 5/6 fake ads (reward event)

static int64_t eyu_now_ms(void) {
    struct timespec ts;
    if (clock_gettime(CLOCK_MONOTONIC, &ts) != 0) return 0;
    return (int64_t)ts.tv_sec * 1000 + ts.tv_nsec / 1000000;
}

static void probe_user_eyu(void *a, void *b, int c) {
    int n = g_user_eyu_count;
    uintptr_t f = (uintptr_t)b;
    uintptr_t caller = (uintptr_t)__builtin_return_address(0);
    int64_t now = eyu_now_ms();

    if (!g_user_base) {
        if (n < 200) LOGI("USER_EYU[%d]: base NON appresa, field=0x%p val=%d caller=0x%p (flush deve arrivare)",
                          n, b, c, (void*)caller);
        g_user_eyu_count = n + 1;
        if (g_orig_user_eyu) g_orig_user_eyu(a, b, c);
        return;
    }

    uintptr_t off = (f >= g_user_base) ? (f - g_user_base) : 0;
    int dk = -1, kind = -1; // kind: 0=daily 1=bonus 2=reset 3=watch
    for (int i = 0; i < 4; i++) {
        if (off == DUNGEON_FIELDS[i].daily) { dk = i; kind = 0; break; }
        if (off == DUNGEON_FIELDS[i].bonus) { dk = i; kind = 1; break; }
        if (off == DUNGEON_FIELDS[i].reset) { dk = i; kind = 2; break; }
        if (off == DUNGEON_FIELDS[i].watch) { dk = i; kind = 3; break; }
    }

    if (dk < 0) {
        if (n < 500) LOGI("USER_EYU[%d]: off=0x%zX val=%d caller=0x%p (non chiave)", n, off, c, (void*)caller);
        g_user_eyu_count = n + 1;
        if (g_orig_user_eyu) g_orig_user_eyu(a, b, c);
        return;
    }

    int old = *(volatile int *)f;   // valore corrente PLAINTEXT nel campo
    int c2 = c;                     // valore da passare all'originale (default invariato)

    if (kind == 2) {
        // write a Last*KeyResetTime -> apre la finestra reset (il reset quotidiano originale)
        if (c > old) {
            g_reset_win_until[dk] = now + KEY_RESET_WINDOW_MS;
            if (n < USER_EYU_MAX_LOG) LOGI("KEYS[dk=%d reset]: LastResetTime old=%d new=%d -> reset window aperta (+%dms)", dk, old, c, KEY_RESET_WINDOW_MS);
        } else if (n < USER_EYU_MAX_LOG) {
            LOGI("KEYS[dk=%d reset]: LastResetTime old=%d new=%d (nessuna finestra)", dk, old, c);
        }
    } else if (kind == 0 || kind == 1) {
        bool in_reset_win = now < g_reset_win_until[dk];
        if (c < old) {
            // DECREMENTO: vittoria (blocca) OPPURE reset daily (permette, finestra aperta)
            if (in_reset_win) {
                if (n < USER_EYU_MAX_LOG) LOGI("KEYS[dk=%d %s]: decremento old=%d new=%d -> RESET WINDOW: passa (reset daily originale)", dk, kind ? "bonus" : "daily", old, c);
            } else {
                c2 = old; // A) le chiavi non scendono mai
                if (n < USER_EYU_MAX_LOG) LOGI("KEYS[A]: %s old=%d new=%d -> BLOCCATO, mantengo %d (vittoria non consuma)", kind ? "bonus" : "daily", old, c, old);
            }
        } else if (c > old && kind == 1) {
            // INCREMENTO bonus: B) se entro la finestra ad-reward, moltiplica l'incremento
            if (g_last_ad_reward_ms && (now - g_last_ad_reward_ms) < 5000) {
                int inc = c - old;
                c2 = old + inc * KEY_BONUS_MULT;
                if (n < USER_EYU_MAX_LOG) LOGI("KEYS[B]: bonus old=%d new=%d inc=%d -> x%d = %d (ad reward)", old, c, inc, KEY_BONUS_MULT, c2);
            } else if (n < USER_EYU_MAX_LOG) {
                LOGI("KEYS[dk=%d bonus]: incremento old=%d new=%d (no ad window, passo)", dk, old, c);
            }
        } else if (n < USER_EYU_MAX_LOG) {
            LOGI("KEYS[dk=%d %s]: write old=%d new=%d (passo)", dk, kind ? "bonus" : "daily", old, c);
        }
    } else if (kind == 3) {
        // WATCH COUNT: C) ads illimitate -> mantieni SEMPRE 0
        if (c != 0) {
            c2 = 0;
            if (n < USER_EYU_MAX_LOG) LOGI("KEYS[C]: watch old=%d new=%d -> FORZO 0 (ads illimitate)", old, c);
        } else if (n < USER_EYU_MAX_LOG) {
            LOGI("KEYS[dk=%d watch]: old=0 (passo)", dk);
        }
    }

    g_user_eyu_count = n + 1;
    if (g_orig_user_eyu) g_orig_user_eyu(a, b, c2);
}

static void install_user_eyu_probe(void) {
    if (!g_code_base) { LOGE("install_user_eyu_probe: no g_code_base (store hook fallito)"); return; }
    uintptr_t eyu_rva = 0x2A85D40; // User.eyu RVA (dump.cs: RVA 0x2A85D40 / Offset 0x2A81D40)
    void *target = (void *)(g_code_base + eyu_rva);
    LOGI("install_user_eyu_probe: hooking User.eyu @%p (g_code_base+0x%X)", target, (unsigned int)eyu_rva);
    void *backup = inline_hooker(target, (void *)probe_user_eyu);
    if (!backup) { LOGE("install_user_eyu_probe: inline_hooker failed for eyu"); return; }
    g_orig_user_eyu = (user_eyu_fn)backup;
    LOGI("install_user_eyu_probe: OK, backup trampoline=%p (A/B/C attivi)", backup);
}


static bool find_lib_path(const char *lib_name, char *out, size_t out_sz) {
    LOGI("find_lib_path: searching for %s", lib_name);
    FILE *f = fopen("/proc/self/maps", "r");
    if (!f) { LOGE("find_lib_path: fopen failed errno=%d (%s)", errno, strerror(errno)); return false; }
    char line[512];
    while (fgets(line, sizeof(line), f)) {
        if (!strstr(line, lib_name)) continue;
        char *p = line;
        int spaces = 0;
        while (*p && spaces < 5) { if (*p == ' ') spaces++; p++; }
        while (*p == ' ') p++;  // skip alignment padding between columns
        char *nl = strchr(p, '\n');
        if (nl) *nl = '\0';
        size_t len = strlen(p);
        if (len > 0 && len < out_sz) {
            memcpy(out, p, len + 1);
            LOGI("find_lib_path: found %s -> %s", lib_name, out);
            fclose(f);
            return true;
        }
    }
    fclose(f);
    LOGI("find_lib_path: %s not found in maps", lib_name);
    return false;
}

// ==================== ELF symbol lookup (bypass linker namespace) ====================

typedef struct {
    unsigned char e_ident[16];
    uint16_t e_type;
    uint16_t e_machine;
    uint32_t e_version;
    uint64_t e_entry;
    uint64_t e_phoff;
    uint64_t e_shoff;
    uint32_t e_flags;
    uint16_t e_ehsize;
    uint16_t e_phentsize;
    uint16_t e_phnum;
    uint16_t e_shentsize;
    uint16_t e_shnum;
    uint16_t e_shstrndx;
} Elf64_Ehdr;

typedef struct {
    uint32_t p_type;
    uint32_t p_flags;
    uint64_t p_offset;
    uint64_t p_vaddr;
    uint64_t p_paddr;
    uint64_t p_filesz;
    uint64_t p_memsz;
    uint64_t p_align;
} Elf64_Phdr;

typedef struct {
    int64_t d_tag;
    union { uint64_t d_val; uint64_t d_ptr; } d_un;
} Elf64_Dyn;

typedef struct {
    uint32_t st_name;
    unsigned char st_info;
    unsigned char st_other;
    uint16_t st_shndx;
    uint64_t st_value;
    uint64_t st_size;
} Elf64_Sym;

typedef struct {
    uint32_t sh_name;
    uint32_t sh_type;
    uint64_t sh_flags;
    uint64_t sh_addr;
    uint64_t sh_offset;
    uint64_t sh_size;
    uint32_t sh_link;
    uint32_t sh_info;
    uint64_t sh_addralign;
    uint64_t sh_entsize;
} Elf64_Shdr;

#define PT_DYNAMIC 2
#define DT_SYMTAB  6
#define DT_STRTAB  5
#define DT_STRSZ   10
#define SHT_SYMTAB 2
#define SHT_STRTAB 3
#define SHT_PROGBITS 1
#define ELF64_ST_BIND(i) ((i) >> 4)

// The game's custom linker maps each LOAD segment at runtime base + file
// offset (not base + p_vaddr). Symbols carry VMA addresses, so the runtime
// address is base + st_value - (p_vaddr - p_offset) of the containing
// segment. These helpers return that gap (0 if the segment is not found).
static int64_t file_segment_gap(FILE *f, const Elf64_Ehdr *ehdr, uint64_t vma) {
    for (int i = 0; i < ehdr->e_phnum; i++) {
        Elf64_Phdr ph;
        fseek(f, ehdr->e_phoff + i * ehdr->e_phentsize, SEEK_SET);
        if (fread(&ph, 1, sizeof(ph), f) != sizeof(ph)) continue;
        if (ph.p_type == 1 && vma >= ph.p_vaddr && vma < ph.p_vaddr + ph.p_memsz) {
            return (int64_t)(ph.p_vaddr - ph.p_offset);
        }
    }
    return 0;
}

static int64_t mem_segment_gap(uintptr_t base, uint64_t vma) {
    auto *ehdr = (Elf64_Ehdr *)base;
    auto *phdr = (Elf64_Phdr *)(base + ehdr->e_phoff);
    for (int i = 0; i < ehdr->e_phnum; i++) {
        if (phdr[i].p_type == 1 && vma >= phdr[i].p_vaddr && vma < phdr[i].p_vaddr + phdr[i].p_memsz) {
            return (int64_t)(phdr[i].p_vaddr - phdr[i].p_offset);
        }
    }
    return 0;
}

static void *elf_find_symbol_from_file(uintptr_t base, const char *name) {
    char path[256];
    if (!find_lib_path("libil2cpp.so", path, sizeof(path))) {
        LOGE("Cannot find libil2cpp.so path in maps");
        return nullptr;
    }
    LOGI("Reading ELF from file (direct): %s", path);

    // Prima prova fopen diretto: un'app puo' normalmente leggere la propria lib in /data/app.
    // Solo se SELinux lo blocca, copia il .so in /data/data via su (mai /data/local/tmp).
    // Nota: /vendor/bin/su NON esiste su Magisk moderno -> usare "su" dal PATH (exit 127 = 32512).
    FILE *f = fopen(path, "rb");
    if (!f) {
        LOGI("direct fopen failed errno=%d (%s) - fallback su copy", errno, strerror(errno));
        char tmp_path[256];
        snprintf(tmp_path, sizeof(tmp_path), "/data/data/com.lavalabs.dungeonrush/files/libil2cpp_drmod.so");
        mkdir("/data/data/com.lavalabs.dungeonrush/files", 0755);
        mkdir("/data/user/0/com.lavalabs.dungeonrush/files", 0755);
        char cmd[512];
        snprintf(cmd, sizeof(cmd), "su -c 'cp \"%s\" %s && chmod 644 %s'", path, tmp_path, tmp_path);
        int rc = system(cmd);
        LOGI("su copy rc=%d", rc);
        if (rc != 0) {
            LOGE("su copy failed");
            return nullptr;
        }
        f = fopen(tmp_path, "rb");
        if (!f) { LOGE("Cannot open %s", tmp_path); return nullptr; }
    }

    Elf64_Ehdr ehdr;
    if (fread(&ehdr, 1, sizeof(ehdr), f) != sizeof(ehdr)) { fclose(f); return nullptr; }
    if (ehdr.e_ident[0] != 0x7f || ehdr.e_ident[1] != 'E') {
        LOGE("File ELF magic mismatch");
        fclose(f);
        return nullptr;
    }

    uint64_t load_bias = 0;
    for (int i = 0; i < ehdr.e_phnum; i++) {
        Elf64_Phdr ph;
        fseek(f, ehdr.e_phoff + i * ehdr.e_phentsize, SEEK_SET);
        if (fread(&ph, 1, sizeof(ph), f) != sizeof(ph)) continue;
        if (ph.p_type == 1 && ph.p_offset == 0) {
            load_bias = base - ph.p_vaddr;
            LOGI("LOAD segment 0: vaddr=0x%lx, load_bias=0x%lx", (unsigned long)ph.p_vaddr, (unsigned long)load_bias);
            break;
        }
    }

    Elf64_Dyn dyn_entry;
    uint64_t dyn_off = 0;
    for (int i = 0; i < ehdr.e_phnum; i++) {
        Elf64_Phdr ph;
        fseek(f, ehdr.e_phoff + i * ehdr.e_phentsize, SEEK_SET);
        if (fread(&ph, 1, sizeof(ph), f) != sizeof(ph)) continue;
        if (ph.p_type == PT_DYNAMIC) { dyn_off = ph.p_offset; break; }
    }
    if (!dyn_off) { LOGE("File: No PT_DYNAMIC"); fclose(f); return nullptr; }

    uint64_t strtab_vma = 0, symtab_vma = 0;
    uint32_t strsz = 0;
    for (uint64_t off = dyn_off;; off += sizeof(dyn_entry)) {
        fseek(f, off, SEEK_SET);
        if (fread(&dyn_entry, 1, sizeof(dyn_entry), f) != sizeof(dyn_entry)) break;
        if (dyn_entry.d_tag == 0) break;
        if (dyn_entry.d_tag == DT_STRTAB) strtab_vma = dyn_entry.d_un.d_ptr;
        if (dyn_entry.d_tag == DT_SYMTAB) symtab_vma = dyn_entry.d_un.d_ptr;
        if (dyn_entry.d_tag == DT_STRSZ) strsz = (uint32_t)dyn_entry.d_un.d_val;
    }
    LOGI("File: strtab_vma=0x%lx symtab_vma=0x%lx strsz=%u", (unsigned long)strtab_vma, (unsigned long)symtab_vma, strsz);

    if (!strtab_vma || !symtab_vma) { LOGE("File: No dynsym/dynstr"); fclose(f); return nullptr; }

    auto vma_to_file_offset = [&](uint64_t vma) -> uint64_t {
        for (int i = 0; i < ehdr.e_phnum; i++) {
            Elf64_Phdr ph;
            fseek(f, ehdr.e_phoff + i * ehdr.e_phentsize, SEEK_SET);
            if (fread(&ph, 1, sizeof(ph), f) != sizeof(ph)) continue;
            if (ph.p_type == 1 && vma >= ph.p_vaddr && vma < ph.p_vaddr + ph.p_memsz) {
                return (vma - ph.p_vaddr) + ph.p_offset;
            }
        }
        return 0;
    };

    uint64_t strtab_file = vma_to_file_offset(strtab_vma);
    uint64_t symtab_file = vma_to_file_offset(symtab_vma);
    if (!strtab_file || !symtab_file) { LOGE("File: cannot convert VMA to file offset"); fclose(f); return nullptr; }

    char *strtab_buf = (char *)malloc(strsz);
    if (!strtab_buf) { fclose(f); return nullptr; }
    fseek(f, strtab_file, SEEK_SET);
    fread(strtab_buf, 1, strsz, f);

    for (uint32_t i = 1; ; i++) {
        Elf64_Sym sym;
        fseek(f, symtab_file + i * sizeof(Elf64_Sym), SEEK_SET);
        if (fread(&sym, 1, sizeof(sym), f) != sizeof(sym)) break;
        if (sym.st_name == 0 && sym.st_value == 0 && sym.st_size == 0) break;
        if (sym.st_name >= strsz) continue;
        if (sym.st_value == 0) continue;
        const char *sname = strtab_buf + sym.st_name;
        if (strcmp(sname, name) == 0) {
            int64_t gap = file_segment_gap(f, &ehdr, sym.st_value);
            void *result = (void *)(load_bias + sym.st_value - gap);
            LOGI("File: found %s at %p (sym_value=0x%lx, bias=0x%lx, gap=0x%lx)",
                 name, result, (unsigned long)sym.st_value, (unsigned long)load_bias, (unsigned long)gap);
            free(strtab_buf);
            fclose(f);
            return result;
        }
    }
    LOGE("File: symbol %s not found", name);
    free(strtab_buf);
    fclose(f);
    return nullptr;
}

static void *elf_find_symbol(uintptr_t base, const char *name) {
    // PRIMA dlsym: la lib e' gia' caricata nel processo e le API il2cpp sono esportate.
    // Nessun parsing ELF, nessun su, nessun file -> immune a PairID/ASLR/permessi.
    void *s = dlsym(RTLD_DEFAULT, name);
    if (!s) {
        void *h = dlopen("libil2cpp.so", RTLD_NOW | RTLD_NOLOAD);
        if (h) s = dlsym(h, name);
    }
    if (s) { LOGI("dlsym: %s = %p", name, s); return s; }
    auto *ehdr = (Elf64_Ehdr *)base;
    if (ehdr->e_ident[0] != 0x7f || ehdr->e_ident[1] != 'E') {
        LOGE("ELF magic mismatch at %p — trying file fallback", (void *)base);
        return elf_find_symbol_from_file(base, name);
    }
    LOGI("ELF OK at %p, phoff=%lu phnum=%d", (void *)base, (unsigned long)ehdr->e_phoff, ehdr->e_phnum);

    auto *phdr = (Elf64_Phdr *)(base + ehdr->e_phoff);
    Elf64_Dyn *dyn = nullptr;
    uint64_t dyn_vaddr = 0;
    for (int i = 0; i < ehdr->e_phnum; i++) {
        if (phdr[i].p_type == PT_DYNAMIC) {
            dyn = (Elf64_Dyn *)(base + phdr[i].p_offset);
            dyn_vaddr = phdr[i].p_vaddr;
            break;
        }
    }
    if (!dyn) { LOGE("No PT_DYNAMIC found"); return nullptr; }
    LOGI("PT_DYNAMIC at vaddr=0x%lx", (unsigned long)dyn_vaddr);

    const char *strtab = nullptr;
    Elf64_Sym *symtab = nullptr;
    uint32_t strsz = 0;
    for (Elf64_Dyn *d = dyn; d->d_tag != 0; d++) {
        if (d->d_tag == DT_STRTAB) { int64_t g = mem_segment_gap(base, d->d_un.d_ptr); strtab = (const char *)(base + d->d_un.d_ptr - g); LOGI("DT_STRTAB=0x%lx (gap=0x%lx)", (unsigned long)d->d_un.d_ptr, (unsigned long)g); }
        if (d->d_tag == DT_SYMTAB) { int64_t g = mem_segment_gap(base, d->d_un.d_ptr); symtab = (Elf64_Sym *)(base + d->d_un.d_ptr - g); LOGI("DT_SYMTAB=0x%lx (gap=0x%lx)", (unsigned long)d->d_un.d_ptr, (unsigned long)g); }
        if (d->d_tag == DT_STRSZ) { strsz = (uint32_t)d->d_un.d_val; LOGI("DT_STRSZ=%u", strsz); }
    }
    if (!strtab || !symtab) {
        LOGE("No dynsym/dynstr (strtab=%p symtab=%p) — trying file fallback", (void*)strtab, (void*)symtab);
        return elf_find_symbol_from_file(base, name);
    }

    uint32_t scanned = 0;
    for (uint32_t i = 0; symtab[i].st_name < strsz; i++) {
        scanned++;
        if (symtab[i].st_value == 0) continue;
        if (ELF64_ST_BIND(symtab[i].st_info) != 1) continue;
        const char *sname = strtab + symtab[i].st_name;
        if (strcmp(sname, name) == 0) {
            int64_t gap = mem_segment_gap(base, symtab[i].st_value);
            void *result = (void *)(base + symtab[i].st_value - gap);
            LOGI("Mem: found %s at %p (sym_value=0x%lx, gap=0x%lx)",
                 name, result, (unsigned long)symtab[i].st_value, (unsigned long)gap);
            return result;
        }
    }
    LOGE("Mem: symbol %s not found (scanned %u entries)", name, scanned);
    return nullptr;
}

// ==================== LSPlant integration ====================

static bool g_lsplant_initialized = false;
static jclass g_hooker_cls = nullptr;
static jmethodID g_hooker_ctor = nullptr;
static jobject g_hooker_callback_method = nullptr;

static void *alloc_exec(size_t size) {
    int fd = (int)syscall(__NR_memfd_create, "drmod", MFD_CLOEXEC);
    if (fd < 0) { LOGE("alloc_exec: memfd_create failed errno=%d", errno); return nullptr; }
    uint8_t zeros[32] = {};
    for (size_t w = 0; w < size; w += sizeof(zeros)) {
        size_t chunk = (size - w) > sizeof(zeros) ? sizeof(zeros) : (size - w);
        if (write(fd, zeros, chunk) < 0) { close(fd); return nullptr; }
    }
    void *p = mmap(nullptr, size, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE, fd, 0);
    close(fd);
    if (p == MAP_FAILED) { LOGE("alloc_exec: mmap failed errno=%d", errno); return nullptr; }
    return p;
}
static void *alloc_exec_near(void *target, size_t size) {
    // First try to allocate in the same 4KB page as target for adrp correctness (offset 0)
    uintptr_t page = (uintptr_t)target & ~0xFFF;
    // Try same page + 0x1000 (next page) first, as original page is occupied
    for (uintptr_t tryAddr = page + 0x1000; tryAddr < page + 0x10000; tryAddr += 0x1000) {
        int fd = (int)syscall(__NR_memfd_create, "drmod_near", MFD_CLOEXEC);
        if (fd < 0) continue;
        uint8_t zeros[32] = {};
        bool ok = true;
        for (size_t w = 0; w < size; w += sizeof(zeros)) {
            size_t chunk = (size - w) > sizeof(zeros) ? sizeof(zeros) : (size - w);
            if (write(fd, zeros, chunk) < 0) { ok = false; break; }
        }
        void *p = nullptr;
        if (ok) p = mmap((void*)tryAddr, size, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_FIXED_NOREPLACE, fd, 0);
        close(fd);
        if (ok && p != MAP_FAILED) {
            LOGI("alloc_exec_near: allocated same-page near %p -> %p", target, p);
            return p;
        }
    }
    uintptr_t tgt = (uintptr_t)target;
    uintptr_t hint = tgt & ~0xFFF;
    for (uintptr_t off = 0x10000; off < 0x8000000; off += 0x10000) {
        for (int dir = -1; dir <= 1; dir += 2) {
            uintptr_t tryAddr = hint + dir * (intptr_t)off;
            if (tryAddr < 0x10000) continue;
            int fd = (int)syscall(__NR_memfd_create, "drmod_near", MFD_CLOEXEC);
            if (fd < 0) continue;
            bool ok = true;
            uint8_t zeros[32] = {};
            for (size_t w = 0; w < size; w += sizeof(zeros)) {
                size_t chunk = (size - w) > sizeof(zeros) ? sizeof(zeros) : (size - w);
                if (write(fd, zeros, chunk) < 0) { ok = false; break; }
            }
            void *p = nullptr;
            if (ok) p = mmap((void*)tryAddr, size, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_FIXED_NOREPLACE, fd, 0);
            close(fd);
            if (ok && p != MAP_FAILED) {
                LOGI("alloc_exec_near: allocated near %p -> %p (offset %ld)", target, p, (long)((intptr_t)p - (intptr_t)target));
                return p;
            }
        }
    }
    LOGI("alloc_exec_near: fallback to alloc_exec");
    return alloc_exec(size);
}
static void fix_adrp_in_trampoline(void *trampoline, void *orig) {
    // Fix PC-relative instructions in the 16-byte copied prologue so they work from
    // the backup trampoline (which can be tens of MB away from the target):
    //  - adrp: convert to ldr-literal with correct target
    //  - b/bl: re-encode imm26 relative to the trampoline copy (range +/-128MB)
    //  - cbz/cbnz, b.cond, tbz/tbnz, adr, ldr-literal: re-encode if target within the
    //    instruction range, else warn (a taken branch to a far target cannot be encoded
    //    directly from the trampoline; dxn's prologue cbz only checks self!=NULL and is
    //    never taken on the reward path)
    uint8_t *t = (uint8_t*)trampoline;
    uintptr_t orig_base = (uintptr_t)orig;
    uintptr_t tramp_base = (uintptr_t)trampoline;
    int literal_idx = 0;
    for (size_t off = 0; off < 16; off += 4) {
        uint32_t ins = *(uint32_t*)(t + off);
        uintptr_t orig_pc = orig_base + off;
        uintptr_t tramp_pc = tramp_base + off;
        // ---- b / bl (imm26, +/-128MB) ----
        if ((ins & 0xFC000000) == 0x94000000 || (ins & 0xFC000000) == 0x14000000) {
            int64_t imm = ins & 0x03FFFFFF;
            if (imm & (1 << 25)) imm |= ~((1LL<<26)-1);
            int64_t orig_target = (int64_t)orig_pc + (imm << 2);
            int64_t rel = orig_target - (int64_t)tramp_pc;
            if (rel < -(1LL<<27) || rel >= (1LL<<27)) {
                LOGE("fix_adrp: b/bl out of range off=%zu orig_target=%p rel=%lld", off, (void*)orig_target, (long long)rel);
                continue;
            }
            uint32_t imm26 = (uint32_t)((rel / 4) & 0x03FFFFFF);
            *(uint32_t*)(t + off) = (ins & 0xFC000000) | imm26;
            LOGI("fix_adrp: patched b/bl @+%zu orig_target=%p rel=%lld bytes", off, (void*)orig_target, (long long)rel);
            continue;
        }
        // ---- cbz / cbnz (imm19, +/-1MB) ----
        if ((ins & 0x7E000000) == 0x34000000) {
            int64_t imm = ins & 0x7FFFF;
            if (imm & (1 << 18)) imm |= ~((1LL<<19)-1);
            int64_t orig_target = (int64_t)orig_pc + (imm << 2);
            int64_t rel = orig_target - (int64_t)tramp_pc;
            if (rel < -(1LL<<20) || rel >= (1LL<<20)) {
                LOGE("fix_adrp: cbz/cbnz @+%zu target %p NOT relocatable (out of +/-1MB) - left as-is", off, (void*)orig_target);
                continue;
            }
            uint32_t imm19 = (uint32_t)((rel / 4) & 0x7FFFF);
            *(uint32_t*)(t + off) = (ins & ~0x00FFFFE0) | (imm19 << 5);
            LOGI("fix_adrp: patched cbz/cbnz @+%zu target=%p", off, (void*)orig_target);
            continue;
        }
        // ---- b.cond (imm19, +/-1MB) ----
        if ((ins & 0xFF000010) == 0x54000000) {
            int64_t imm = ins & 0x7FFFF;
            if (imm & (1 << 18)) imm |= ~((1LL<<19)-1);
            int64_t orig_target = (int64_t)orig_pc + (imm << 2);
            int64_t rel = orig_target - (int64_t)tramp_pc;
            if (rel < -(1LL<<20) || rel >= (1LL<<20)) {
                LOGE("fix_adrp: b.cond @+%zu target %p NOT relocatable (out of +/-1MB) - left as-is", off, (void*)orig_target);
                continue;
            }
            uint32_t imm19 = (uint32_t)((rel / 4) & 0x7FFFF);
            *(uint32_t*)(t + off) = (ins & 0xFF00001F) | (imm19 << 5);
            LOGI("fix_adrp: patched b.cond @+%zu target=%p", off, (void*)orig_target);
            continue;
        }
        // ---- tbz / tbnz (imm14, +/-32KB) ----
        if ((ins & 0x7E000000) == 0x36000000) {
            int64_t imm = ins & 0x3FFF;
            if (imm & (1 << 13)) imm |= ~((1LL<<14)-1);
            int64_t orig_target = (int64_t)orig_pc + (imm << 2);
            int64_t rel = orig_target - (int64_t)tramp_pc;
            if (rel < -(1LL<<15) || rel >= (1LL<<15)) {
                LOGE("fix_adrp: tbz/tbnz @+%zu target %p NOT relocatable (out of +/-32KB) - left as-is", off, (void*)orig_target);
                continue;
            }
            uint32_t imm14 = (uint32_t)((rel / 4) & 0x3FFF);
            *(uint32_t*)(t + off) = (ins & ~0x0007FFE0) | (imm14 << 5);
            LOGI("fix_adrp: patched tbz/tbnz @+%zu target=%p", off, (void*)orig_target);
            continue;
        }
        // ---- adr (imm21, +/-1MB) ----
        if ((ins & 0x9F000000) == 0x10000000) {
            int immlo = (ins >> 29) & 0x3;
            int immhi = (ins >> 5) & 0x7FFFF;
            int64_t imm = ((int64_t)immhi << 2) | immlo;
            if (imm & (1 << 20)) imm |= ~((1LL<<21)-1);
            int64_t orig_target = (int64_t)orig_pc + imm;
            int64_t rel = orig_target - (int64_t)tramp_pc;
            if (rel < -(1LL<<20) || rel >= (1LL<<20)) {
                LOGE("fix_adrp: adr @+%zu target %p NOT relocatable (out of +/-1MB) - left as-is", off, (void*)orig_target);
                continue;
            }
            uint32_t immlo2 = (uint32_t)(rel & 0x3);
            uint32_t immhi2 = (uint32_t)((rel >> 2) & 0x7FFFF);
            *(uint32_t*)(t + off) = (ins & 0x9F00001F) | (immlo2 << 29) | (immhi2 << 5);
            LOGI("fix_adrp: patched adr @+%zu target=%p", off, (void*)orig_target);
            continue;
        }
        // ---- adrp (page-quantised, via literal pool) ----
        if ((ins & 0x9F000000) == 0x90000000) {
            int rd = ins & 0x1F;
            int immlo = (ins >> 29) & 0x3;
            int immhi = (ins >> 5) & 0x7FFFF;
            int64_t imm = ((int64_t)immhi << 2) | immlo;
            if (imm & (1 << 20)) imm |= ~((1LL<<21)-1);
            uintptr_t orig_page = (orig_base + off) & ~0xFFF;
            uintptr_t orig_target = orig_page + (imm << 12);
            uintptr_t literal_addr = tramp_base + 0x20 + literal_idx * 8;
            int64_t dist = (int64_t)literal_addr - (int64_t)(tramp_base + off);
            int64_t imm19 = dist / 4;
            if (imm19 < -(1<<18) || imm19 >= (1<<18)) {
                LOGE("fix_adrp: distance out of range off=%zu dist=%lld", off, (long long)dist);
                continue;
            }
            uint32_t ldr = 0x58000000 | ((imm19 & 0x7FFFF) << 5) | rd;
            *(uint32_t*)(t + off) = ldr;
            *(uintptr_t*)(t + 0x20 + literal_idx * 8) = orig_target;
            LOGI("fix_adrp: patched adrp x%d @+%zu orig %p target %p -> ldr literal #%d dist %lld", rd, off, (void*)(orig_base+off), (void*)orig_target, literal_idx, (long long)dist);
            literal_idx++;
            continue;
        }
        // ---- ldr-literal (imm19, +/-1MB) ----
        if ((ins & 0x3B000000) == 0x18000000) {
            int64_t imm = ins & 0x7FFFF;
            if (imm & (1 << 18)) imm |= ~((1LL<<19)-1);
            int64_t orig_target = (int64_t)orig_pc + (imm << 2);
            int64_t rel = orig_target - (int64_t)tramp_pc;
            if (rel < -(1LL<<20) || rel >= (1LL<<20)) {
                LOGE("fix_adrp: ldr-literal @+%zu target %p NOT relocatable (out of +/-1MB) - left as-is", off, (void*)orig_target);
                continue;
            }
            uint32_t imm19 = (uint32_t)((rel / 4) & 0x7FFFF);
            *(uint32_t*)(t + off) = (ins & ~0x00FFFFE0) | (imm19 << 5);
            LOGI("fix_adrp: patched ldr-literal @+%zu target=%p", off, (void*)orig_target);
            continue;
        }
    }
    if (literal_idx) __builtin___clear_cache((char*)trampoline, (char*)trampoline + 64);
}

struct HookRecord {
    void *target;
    uint8_t orig[16];
    void *backup_exec;
};
static HookRecord g_hook_records[16];
static int g_hook_count = 0;

static void *inline_hooker(void *target, void *hooker_fn) {
    if (!target || !hooker_fn) { LOGE("inline_hooker: null target=%p hooker=%p", target, hooker_fn); return nullptr; }
    LOGI("inline_hooker: target=%p hooker=%p", target, hooker_fn);
    if (g_hook_count >= 16) { LOGE("inline_hooker: too many hooks"); return nullptr; }

    HookRecord &rec = g_hook_records[g_hook_count];
    rec.target = target;
    memcpy(rec.orig, target, 16);

    size_t backup_size = 64;
    void *backup = alloc_exec_near(target, backup_size);
    if (!backup) { LOGE("inline_hooker: alloc_exec_near backup failed"); return nullptr; }
    rec.backup_exec = backup;

    memcpy(backup, rec.orig, 16);
    // Fix adrp PC-relative before adding jump back (literals at 0x20+)
    fix_adrp_in_trampoline(backup, target);
    uint64_t ret_addr = (uint64_t)target + 16;
    uint32_t *bak = (uint32_t *)backup;
    bak[4] = 0x58000050;
    bak[5] = 0xD61F0200;
    *(uint64_t *)&bak[6] = ret_addr;
    __builtin___clear_cache((char *)backup, (char *)backup + backup_size);

    long ps = sysconf(_SC_PAGESIZE);
    uintptr_t pg = (uintptr_t)target & ~(ps - 1);
    if (mprotect((void *)pg, ps, PROT_READ | PROT_WRITE | PROT_EXEC) != 0) {
        LOGE("inline_hooker: mprotect RWX failed at %p", target);
        return nullptr;
    }

    uint64_t hooker_addr = (uint64_t)hooker_fn;
    uint32_t *tgt = (uint32_t *)target;
    tgt[0] = 0x58000050;
    tgt[1] = 0xD61F0200;
    *(uint64_t *)&tgt[2] = hooker_addr;
    __builtin___clear_cache((char *)target, (char *)target + 16);

    mprotect((void *)pg, ps, PROT_READ | PROT_EXEC);
    g_hook_count++;
    LOGI("inline_hooker: backup=%p (exec)", backup);
    return backup;
}

static bool inline_unhooker(void *func) {
    for (int i = 0; i < g_hook_count; i++) {
        if (g_hook_records[i].backup_exec == func) {
            void *target = g_hook_records[i].target;
            long ps = sysconf(_SC_PAGESIZE);
            uintptr_t pg = (uintptr_t)target & ~(ps - 1);
            mprotect((void *)pg, ps, PROT_READ | PROT_WRITE | PROT_EXEC);
            memcpy(target, g_hook_records[i].orig, 16);
            __builtin___clear_cache((char *)target, (char *)target + 16);
            mprotect((void *)pg, ps, PROT_READ | PROT_EXEC);
            LOGI("inline_unhooker: restored %p", target);
            return true;
        }
    }
    LOGE("inline_unhooker: record not found for %p", func);
    return false;
}

static uintptr_t g_libart_base = 0;
static void *g_libart_handle = nullptr;

static uintptr_t find_libart_base() {
    if (g_libart_base) return g_libart_base;
    FILE *f = fopen("/proc/self/maps", "r");
    if (!f) return 0;
    char line[512];
    while (fgets(line, sizeof(line), f)) {
        if (!strstr(line, "libart.so")) continue;
        uintptr_t start = 0;
        unsigned long foff = 0;
        char perms[8] = {};
        if (sscanf(line, "%lx-%*x %7s %lx", &start, perms, &foff) == 3) {
            if (strchr(perms, 'x')) {
                g_libart_base = start - foff;
                LOGI("find_libart_base: libart.so base=%p (start=%p foff=0x%lx)", (void *)g_libart_base, (void *)start, foff);
                fclose(f);
                return g_libart_base;
            }
        }
    }
    fclose(f);
    return 0;
}

static void *libart_resolver(const char *name) {
    if (!g_libart_handle) {
        g_libart_handle = dlopen("libart.so", RTLD_NOLOAD);
        LOGI("libart_resolver: dlopen libart.so = %p", g_libart_handle);
    }
    if (g_libart_handle) {
        void *sym = dlsym(g_libart_handle, name);
        if (sym) { LOGI("libart_resolver: %s = %p", name, sym); return sym; }
    }
    // GetMethodShorty is a local (.gnu_debugdata) symbol not exported in dynsym.
    // Runtime XZ-decompression of .gnu_debugdata via Samsung's liblzma.so crashes
    // (null function-pointer in CrcCalc). Instead use the hardcoded offset extracted
    // OFFLINE from this exact firmware (A217FXXSCDXE2 / Android 12) using 7-zip.
    // uint local vaddr of _ZN3artL15GetMethodShortyEP7_JNIEnvP10_jmethodID = 0x92AA88.
    if (strstr(name, "GetMethodShorty")) {
        uintptr_t b = find_libart_base();
        if (b) {
            void *r = (void *)(b + 0x92AA88);
            LOGI("libart_resolver: %s = %p (hardcoded offset 0x92AA88)", name, r);
            return r;
        }
    }
    // ClassLinker::Init needs RegisterNative/UnregisterNative (all variants are
    // local .gnu_debugdata symbols, not in dynsym). Use ClassLinker variants.
    // _ZN3art11ClassLinker14RegisterNativeEPNS_6ThreadEPNS_9ArtMethodEPKv = 0x383DA8
    if (strstr(name, "RegisterNative")) {
        uintptr_t b = find_libart_base();
        if (b) {
            void *r = (void *)(b + 0x383DA8);
            LOGI("libart_resolver: %s = %p (hardcoded offset 0x383DA8)", name, r);
            return r;
        }
    }
    // _ZN3art11ClassLinker16UnregisterNativeEPNS_6ThreadEPNS_9ArtMethodE = 0x80A334
    if (strstr(name, "UnregisterNative")) {
        uintptr_t b = find_libart_base();
        if (b) {
            void *r = (void *)(b + 0x80A334);
            LOGI("libart_resolver: %s = %p (hardcoded offset 0x80A334)", name, r);
            return r;
        }
    }
    // ClassLinker::Init deopt fallback when SetEntryPointsToInterpreter /
    // GetOptimizedCodeFor are absent. Plain (non-mangled) data symbol in .gnu_debugdata.
    // art_quick_to_interpreter_bridge = 0x2C2480
    if (strcmp(name, "art_quick_to_interpreter_bridge") == 0) {
        uintptr_t b = find_libart_base();
        if (b) {
            void *r = (void *)(b + 0x2C2480);
            LOGI("libart_resolver: %s = %p (hardcoded offset 0x2C2480)", name, r);
            return r;
        }
    }
    uintptr_t base = find_libart_base();
    if (!base) { LOGE("libart_resolver: no libart base"); return nullptr; }
    auto *ehdr = (Elf64_Ehdr *)base;
    if (ehdr->e_ident[0] != 0x7f || ehdr->e_ident[1] != 'E') {
        LOGE("libart_resolver: bad ELF magic"); return nullptr;
    }
    auto *phdr = (Elf64_Phdr *)(base + ehdr->e_phoff);
    const char *strtab = nullptr;
    Elf64_Sym *symtab = nullptr;
    uint32_t strsz = 0;
    for (int i = 0; i < ehdr->e_phnum; i++) {
        if (phdr[i].p_type == PT_DYNAMIC) {
            auto *dyn = (Elf64_Dyn *)(base + phdr[i].p_offset);
            for (Elf64_Dyn *d = dyn; d->d_tag != 0; d++) {
                if (d->d_tag == DT_STRTAB) { int64_t g = mem_segment_gap(base, d->d_un.d_ptr); strtab = (const char *)(base + d->d_un.d_ptr - g); }
                if (d->d_tag == DT_SYMTAB) { int64_t g = mem_segment_gap(base, d->d_un.d_ptr); symtab = (Elf64_Sym *)(base + d->d_un.d_ptr - g); }
                if (d->d_tag == DT_STRSZ) strsz = (uint32_t)d->d_un.d_val;
            }
            break;
        }
    }
    if (strtab && symtab) {
        for (uint32_t i = 1; symtab[i].st_name < strsz; i++) {
            if (symtab[i].st_value == 0) continue;
            const char *sname = strtab + symtab[i].st_name;
            if (strcmp(sname, name) == 0) {
                int64_t gap = mem_segment_gap(base, symtab[i].st_value);
                void *result = (void *)(base + symtab[i].st_value - gap);
                LOGI("libart_resolver: %s = %p (dynsym)", name, result);
                return result;
            }
        }
        LOGI("libart_resolver: %s not in dynsym, scanning for substring 'Shorty'...", name);
        void *substr_result = nullptr;
        for (uint32_t i = 1; symtab[i].st_name < strsz; i++) {
            if (symtab[i].st_value == 0) continue;
            const char *sname = strtab + symtab[i].st_name;
            if (strstr(sname, "Shorty")) {
                int64_t gap = mem_segment_gap(base, symtab[i].st_value);
                void *r = (void *)(base + symtab[i].st_value - gap);
                LOGI("libart_resolver: substring 'Shorty' match: %s = %p", sname, r);
                if (strstr(sname, "GetMethodShorty")) {
                    substr_result = r;
                    break;
                }
                if (!substr_result) substr_result = r;
            }
        }
        if (substr_result) {
            LOGI("libart_resolver: returning substring match %p for %s", substr_result, name);
            return substr_result;
        }
    }
    // File-based .gnu_debugdata fallback (below) is DISABLED: runtime XZ decompression
    // via Samsung's liblzma.so (XzUnpacker_Code -> CrcCalc) segfaults (null fn-ptr) and
    // kills the process for ANY symbol not found in dynsym. All REQUIRED local symbols
    // (GetMethodShorty, RegisterNative, UnregisterNative, art_quick_to_interpreter_bridge)
    // are already hardcoded above; any other unresolved symbol has a non-fatal fallback
    // in LSPlant, so returning nullptr here is safe and avoids the crash.
    LOGI("libart_resolver: %s not in dynsym and not hardcoded; returning nullptr (XZ fallback disabled)", name);
    return nullptr;
    char fpath[256];
    if (!find_lib_path("libart.so", fpath, sizeof(fpath))) {
        LOGE("libart_resolver: cannot find libart.so path"); return nullptr;
    }
    LOGI("libart_resolver: libart.so path = %s", fpath);
    char tmp[] = "/data/data/com.lavalabs.dungeonrush/files/libart_drmod.so";
    mkdir("/data/data/com.lavalabs.dungeonrush/files", 0755);
    mkdir("/data/user/0/com.lavalabs.dungeonrush/files", 0755);
    char cmd[512];
    snprintf(cmd, sizeof(cmd), "/vendor/bin/su -c 'cp \"%s\" %s && chmod 644 %s'", fpath, tmp, tmp);
    int cprc = system(cmd);
    LOGI("libart_resolver: su copy rc=%d", cprc);
    if (cprc != 0) { LOGE("libart_resolver: su copy failed rc=%d", cprc); return nullptr; }
    FILE *f = fopen(tmp, "rb");
    if (!f) { LOGE("libart_resolver: cannot open %s", tmp); return nullptr; }
    fseek(f, 0, SEEK_END);
    long fsize = ftell(f);
    fseek(f, 0, SEEK_SET);
    LOGI("libart_resolver: file size=%ld bytes", fsize);
    Elf64_Ehdr fehdr;
    if (fread(&fehdr, 1, sizeof(fehdr), f) != sizeof(fehdr) || fehdr.e_ident[0] != 0x7f) {
        LOGE("libart_resolver: bad file ELF"); fclose(f); return nullptr;
    }
    LOGI("libart_resolver: ELF shnum=%u shoff=0x%lx shstrndx=%u phnum=%u", fehdr.e_shnum, (unsigned long)fehdr.e_shoff, fehdr.e_shstrndx, fehdr.e_phnum);
    if (fehdr.e_shnum == 0 || fehdr.e_shoff == 0) {
        LOGE("libart_resolver: no section headers in file (shnum=%u shoff=0x%lx) — symbol might be stripped", fehdr.e_shnum, (unsigned long)fehdr.e_shoff);
        fclose(f);
        return nullptr;
    }
    uint64_t load_bias = 0;
    for (int i = 0; i < fehdr.e_phnum; i++) {
        Elf64_Phdr ph;
        fseek(f, fehdr.e_phoff + i * fehdr.e_phentsize, SEEK_SET);
        if (fread(&ph, 1, sizeof(ph), f) != sizeof(ph)) continue;
        if (ph.p_type == 1 && ph.p_offset == 0) {
            load_bias = base - ph.p_vaddr;
            break;
        }
    }
    LOGI("libart_resolver: load_bias=0x%lx", (unsigned long)load_bias);
    Elf64_Shdr shstrtab_hdr;
    fseek(f, fehdr.e_shoff + fehdr.e_shstrndx * fehdr.e_shentsize, SEEK_SET);
    if (fread(&shstrtab_hdr, 1, sizeof(shstrtab_hdr), f) != sizeof(shstrtab_hdr)) {
        LOGE("libart_resolver: failed to read shstrtab header"); fclose(f); return nullptr;
    }
    char *shstrtab = (char *)malloc(shstrtab_hdr.sh_size);
    if (!shstrtab) { fclose(f); return nullptr; }
    fseek(f, shstrtab_hdr.sh_offset, SEEK_SET);
    fread(shstrtab, 1, shstrtab_hdr.sh_size, f);

    // === Phase 1: scan section headers for .gnu_debugdata (compressed mini symtab) ===
    void *gnu_debugdata_result = nullptr;
    for (int i = 0; i < fehdr.e_shnum; i++) {
        Elf64_Shdr shdr;
        fseek(f, fehdr.e_shoff + i * fehdr.e_shentsize, SEEK_SET);
        if (fread(&shdr, 1, sizeof(shdr), f) != sizeof(shdr)) continue;
        const char *secname = (shdr.sh_name < shstrtab_hdr.sh_size) ? shstrtab + shdr.sh_name : "";
        LOGI("libart_resolver: section[%u] type=%u name='%s' offset=0x%lx size=0x%lx",
             i, shdr.sh_type, secname, (unsigned long)shdr.sh_offset, (unsigned long)shdr.sh_size);
        if (shdr.sh_type == SHT_PROGBITS && strcmp(secname, ".gnu_debugdata") == 0) {
            LOGI("libart_resolver: .gnu_debugdata found! offset=0x%lx size=0x%lx",
                 (unsigned long)shdr.sh_offset, (unsigned long)shdr.sh_size);
            uint8_t *secdata = (uint8_t *)malloc(shdr.sh_size);
            if (!secdata) continue;
            fseek(f, shdr.sh_offset, SEEK_SET);
            if (fread(secdata, 1, shdr.sh_size, f) == shdr.sh_size) {
                LOGI("libart_resolver: .gnu_debugdata first bytes: %02x %02x %02x %02x %02x %02x %02x %02x",
                     secdata[0], secdata[1], secdata[2], secdata[3], secdata[4], secdata[5], secdata[6], secdata[7]);
                // Try LZ4 decompress (0x04 0x22 0x4D 0x18 = LZ4 frame magic)
                if (secdata[0] == 0x04 && secdata[1] == 0x22 && secdata[2] == 0x4D && secdata[3] == 0x18) {
                    LOGI("libart_resolver: .gnu_debugdata is LZ4 compressed (frame magic detected)");
                }
                // XZ decompress (FD 37 7A 58 5A 00) via liblzma.so (7-zip SDK XzUnpacker)
                else if (secdata[0] == 0xFD && secdata[1] == 0x37 && secdata[2] == 0x7A && secdata[3] == 0x58) {
                    LOGI("libart_resolver: .gnu_debugdata is XZ compressed, decompressing via XzUnpacker_Code...");
                    typedef unsigned char LzByte;
                    typedef size_t LzSizeT;
                    typedef int LzRes;
                    typedef int LzmaStatus;
                    static void *(*p_XzUnpacker_Create)(void *(*)(size_t), void (*)(void*)) = nullptr;
                    static void (*p_XzUnpacker_Free)(void*, void (*)(void*)) = nullptr;
                    static LzRes (*p_XzUnpacker_Code)(void*, LzByte*, LzSizeT*, const LzByte*, LzSizeT*, int, int, LzmaStatus*) = nullptr;
                    static void (*p_XzUnpacker_Init)(void*) = nullptr;
                    static void *liblzma = nullptr;
                    static const char *liblzma_paths[] = {
                        "/system/lib64/liblzma.so",
                        "/apex/com.android.art/lib64/liblzma.so",
                        nullptr
                    };
                    if (!liblzma) {
                        for (int li = 0; liblzma_paths[li]; li++) {
                            liblzma = dlopen(liblzma_paths[li], RTLD_NOW);
                            LOGI("libart_resolver: dlopen %s = %p", liblzma_paths[li], liblzma);
                            if (liblzma) {
                                p_XzUnpacker_Create = (decltype(p_XzUnpacker_Create))dlsym(liblzma, "XzUnpacker_Create");
                                p_XzUnpacker_Free = (decltype(p_XzUnpacker_Free))dlsym(liblzma, "XzUnpacker_Free");
                                p_XzUnpacker_Code = (decltype(p_XzUnpacker_Code))dlsym(liblzma, "XzUnpacker_Code");
                                p_XzUnpacker_Init = (decltype(p_XzUnpacker_Init))dlsym(liblzma, "XzUnpacker_Init");
                                LOGI("libart_resolver: XzUnpacker Create=%p Free=%p Code=%p Init=%p",
                                     (void*)p_XzUnpacker_Create, (void*)p_XzUnpacker_Free, (void*)p_XzUnpacker_Code, (void*)p_XzUnpacker_Init);
                                if (p_XzUnpacker_Free && p_XzUnpacker_Code) break;
                                dlclose(liblzma);
                                liblzma = nullptr;
                                p_XzUnpacker_Create = nullptr;
                                p_XzUnpacker_Free = nullptr;
                                p_XzUnpacker_Code = nullptr;
                                p_XzUnpacker_Init = nullptr;
                            }
                        }
                    }
                    if (p_XzUnpacker_Free && p_XzUnpacker_Code) {
                        size_t xz_len = shdr.sh_size;
                        void *unpacker = nullptr;
                        if (p_XzUnpacker_Create) {
                            unpacker = p_XzUnpacker_Create(malloc, free);
                        } else if (p_XzUnpacker_Init) {
                            unpacker = malloc(65536);
                            if (unpacker) { memset(unpacker, 0, 65536); p_XzUnpacker_Init(unpacker); }
                        } else {
                            unpacker = malloc(65536);
                            if (unpacker) memset(unpacker, 0, 65536);
                        }
                        LOGI("libart_resolver: XzUnpacker unpacker=%p (xz_len=%zu)", unpacker, xz_len);
                        if (unpacker) {
                            size_t out_cap = xz_len * 20;
                            if (out_cap < 0x10000) out_cap = 0x10000;
                            uint8_t *outbuf = (uint8_t *)malloc(out_cap);
                            if (outbuf) {
                                LzSizeT destLen = out_cap;
                                LzSizeT srcLen = xz_len;
                                LzmaStatus lzstatus = {};
                                LOGI("libart_resolver: calling XzUnpacker_Code(destCap=%zu, srcLen=%zu)...", out_cap, xz_len);
                                LzRes lzret = p_XzUnpacker_Code(unpacker, outbuf, &destLen, secdata, &srcLen, 1 /*srcFinished*/, 1 /*FINISH_END*/, &lzstatus);
                                LOGI("libart_resolver: XzUnpacker_Code ret=%d status=%d outLen=%zu", lzret, lzstatus, destLen);
                                if ((lzret == 0 || lzret == 1) && destLen > 0) {
                                    LOGI("libart_resolver: XZ decompress OK, %zu bytes", destLen);
                                    size_t out_len = destLen;
                                    if (out_len >= sizeof(Elf64_Ehdr) && outbuf[0] == 0x7f && outbuf[1] == 'E') {
                                        auto *de = (Elf64_Ehdr *)outbuf;
                                        auto *dp = (Elf64_Phdr *)(outbuf + de->e_phoff);
                                        uint64_t dbias = load_bias;
                                        for (int pi = 0; pi < de->e_phnum; pi++) {
                                            if (dp[pi].p_type == 1 && dp[pi].p_offset == 0) {
                                                dbias = base - dp[pi].p_vaddr;
                                                break;
                                            }
                                        }
                                        for (int pi = 0; pi < de->e_phnum; pi++) {
                                            if (dp[pi].p_type != PT_DYNAMIC) continue;
                                            auto *ddyn = (Elf64_Dyn *)(outbuf + dp[pi].p_offset);
                                            const char *dstrtab = nullptr;
                                            Elf64_Sym *dsymtab = nullptr;
                                            uint32_t dstrsz = 0;
                                            for (auto *dd = ddyn; dd->d_tag != 0; dd++) {
                                                if (dd->d_tag == DT_STRTAB) dstrtab = (const char *)(outbuf + dd->d_un.d_ptr);
                                                if (dd->d_tag == DT_SYMTAB) dsymtab = (Elf64_Sym *)(outbuf + dd->d_un.d_ptr);
                                                if (dd->d_tag == DT_STRSZ) dstrsz = dd->d_un.d_val;
                                            }
                                            if (dstrtab && dsymtab) {
                                                LOGI("libart_resolver: XZ mini ELF dynsym found, scanning...");
                                                for (uint32_t si = 1; dsymtab[si].st_name < dstrsz; si++) {
                                                    if (dsymtab[si].st_value == 0) continue;
                                                    const char *sn = dstrtab + dsymtab[si].st_name;
                                                    if (strstr(sn, "Shorty") || strstr(sn, "ethodShorty")) {
                                                        int64_t g = mem_segment_gap(base, dsymtab[si].st_value);
                                                        void *r = (void *)(dbias + dsymtab[si].st_value - g);
                                                        LOGI("libart_resolver: XZ debugdata match: %s = %p", sn, r);
                                                        if (strstr(sn, "GetMethodShorty") || strstr(sn, "15GetMethodShorty")) {
                                                            gnu_debugdata_result = r;
                                                        }
                                                    }
                                                }
                                            }
                                            break;
                                        }
                                    }
                                } else {
                                    LOGI("libart_resolver: XZ decompress failed ret=%u status=%u", (unsigned)lzret, (unsigned)lzstatus);
                                }
                                free(outbuf);
                            }
                            p_XzUnpacker_Free(unpacker, free);
                        }
                    } else {
                        LOGI("libart_resolver: XzUnpacker symbols not found in liblzma.so, skipping XZ decompress");
                    }
                }
                // Try zlib (78 01 or 78 9C or 78 DA)
                else if (secdata[0] == 0x78 && (secdata[1] == 0x01 || secdata[1] == 0x9C || secdata[1] == 0xDA)) {
                    LOGI("libart_resolver: .gnu_debugdata is zlib compressed, attempting decompress...");
                    z_stream strm = {};
                    strm.next_in = secdata;
                    strm.avail_in = shdr.sh_size;
                    if (inflateInit(&strm) == Z_OK) {
                        size_t out_cap = shdr.sh_size * 10;
                        uint8_t *outbuf = (uint8_t *)malloc(out_cap);
                        if (outbuf) {
                            strm.next_out = outbuf;
                            strm.avail_out = out_cap;
                            int zret = inflate(&strm, Z_FINISH);
                            if (zret == Z_STREAM_END || zret >= 0) {
                                size_t out_len = out_cap - strm.avail_out;
                                LOGI("libart_resolver: zlib decompress OK, %zu bytes", out_len);
                                // Parse the decompressed ELF
                                if (out_len >= sizeof(Elf64_Ehdr) && outbuf[0] == 0x7f && outbuf[1] == 'E') {
                                    auto *de = (Elf64_Ehdr *)outbuf;
                                    auto *dp = (Elf64_Phdr *)(outbuf + de->e_phoff);
                                    uint64_t dbias = load_bias;
                                    for (int pi = 0; pi < de->e_phnum; pi++) {
                                        if (dp[pi].p_type == 1 && dp[pi].p_offset == 0) {
                                            dbias = base - dp[pi].p_vaddr;
                                            break;
                                        }
                                    }
                                    // Find PT_DYNAMIC in the mini ELF
                                    for (int pi = 0; pi < de->e_phnum; pi++) {
                                        if (dp[pi].p_type != PT_DYNAMIC) continue;
                                        auto *ddyn = (Elf64_Dyn *)(outbuf + dp[pi].p_offset);
                                        const char *dstrtab = nullptr;
                                        Elf64_Sym *dsymtab = nullptr;
                                        uint32_t dstrsz = 0;
                                        for (auto *dd = ddyn; dd->d_tag != 0; dd++) {
                                            if (dd->d_tag == DT_STRTAB) dstrtab = (const char *)(outbuf + dd->d_un.d_ptr);
                                            if (dd->d_tag == DT_SYMTAB) dsymtab = (Elf64_Sym *)(outbuf + dd->d_un.d_ptr);
                                            if (dd->d_tag == DT_STRSZ) dstrsz = dd->d_un.d_val;
                                        }
                                        if (dstrtab && dsymtab) {
                                            LOGI("libart_resolver: mini ELF dynsym found, scanning...");
                                            for (uint32_t si = 1; dsymtab[si].st_name < dstrsz; si++) {
                                                if (dsymtab[si].st_value == 0) continue;
                                                const char *sn = dstrtab + dsymtab[si].st_name;
                                                if (strstr(sn, "Shorty") || strstr(sn, "ethodShorty")) {
                                                    int64_t g = mem_segment_gap(base, dsymtab[si].st_value);
                                                    void *r = (void *)(dbias + dsymtab[si].st_value - g);
                                                    LOGI("libart_resolver: .gnu_debugdata match: %s = %p", sn, r);
                                                    if (strstr(sn, "GetMethodShorty") || strstr(sn, "15GetMethodShorty")) {
                                                        gnu_debugdata_result = r;
                                                    }
                                                }
                                            }
                                        }
                                        break;
                                    }
                                }
                            } else {
                                LOGI("libart_resolver: zlib decompress failed ret=%d", zret);
                            }
                            inflateEnd(&strm);
                            free(outbuf);
                        }
                    }
                }
                // Try raw/uncompressed - if first bytes look like ELF
                else if (secdata[0] == 0x7f && secdata[1] == 'E') {
                    LOGI("libart_resolver: .gnu_debugdata is uncompressed ELF");
                }
                else {
                    LOGI("libart_resolver: .gnu_debugdata unknown compression (magic: %02x %02x %02x %02x)",
                         secdata[0], secdata[1], secdata[2], secdata[3]);
                }
            }
            free(secdata);
        }
    }
    if (gnu_debugdata_result) {
        free(shstrtab);
        fclose(f);
        LOGI("libart_resolver: returning .gnu_debugdata result %p", gnu_debugdata_result);
        return gnu_debugdata_result;
    }

    // === Phase 2: scan file .symtab ===
    int symtab_count = 0;
    for (int i = 0; i < fehdr.e_shnum; i++) {
        Elf64_Shdr shdr;
        fseek(f, fehdr.e_shoff + i * fehdr.e_shentsize, SEEK_SET);
        if (fread(&shdr, 1, sizeof(shdr), f) != sizeof(shdr)) continue;
        if (shdr.sh_type == SHT_SYMTAB) {
            symtab_count++;
        }
    }
    LOGI("libart_resolver: found %d SHT_SYMTAB sections", symtab_count);
    if (symtab_count == 0) {
        LOGE("libart_resolver: no .symtab section — symbol likely stripped from libart.so");
        // Phase 3: in-memory code scan — search for function by known call pattern
        // LSPlant calls GetMethodShorty via our resolver. If we can't find it by name,
        // scan the in-memory dynsym for ANY symbol containing a substring match
        LOGI("libart_resolver: Phase 3 — scanning in-memory dynsym for substring matches of '%s'", name);
        if (strtab && symtab) {
            const char *search_term = "Shorty";
            if (!strstr(name, "Shorty")) search_term = name;
            for (uint32_t i = 1; symtab[i].st_name < strsz; i++) {
                if (symtab[i].st_value == 0) continue;
                const char *sname = strtab + symtab[i].st_name;
                if (strstr(sname, search_term)) {
                    int64_t gap = mem_segment_gap(base, symtab[i].st_value);
                    void *result = (void *)(base + symtab[i].st_value - gap);
                    LOGI("libart_resolver: substring match: %s = %p", sname, result);
                    if (strstr(sname, "GetMethodShorty") || strstr(sname, "15GetMethodShorty")) {
                        free(shstrtab);
                        fclose(f);
                        return result;
                    }
                }
            }
        }
        free(shstrtab);
        fclose(f);
        LOGE("libart_resolver: %s NOT FOUND anywhere (all phases exhausted)", name);
        return nullptr;
    }
    for (int i = 0; i < fehdr.e_shnum; i++) {
        Elf64_Shdr shdr;
        fseek(f, fehdr.e_shoff + i * fehdr.e_shentsize, SEEK_SET);
        if (fread(&shdr, 1, sizeof(shdr), f) != sizeof(shdr)) continue;
        if (shdr.sh_type != SHT_SYMTAB) continue;
        Elf64_Shdr strsh;
        fseek(f, fehdr.e_shoff + shdr.sh_link * fehdr.e_shentsize, SEEK_SET);
        if (fread(&strsh, 1, sizeof(strsh), f) != sizeof(strsh)) continue;
        char *strtab_buf = (char *)malloc(strsh.sh_size);
        if (!strtab_buf) continue;
        fseek(f, strsh.sh_offset, SEEK_SET);
        fread(strtab_buf, 1, strsh.sh_size, f);
        uint32_t nsym = shdr.sh_size / shdr.sh_entsize;
        LOGI("libart_resolver: scanning symtab section %u: %u symbols", i, nsym);
        for (uint32_t j = 0; j < nsym; j++) {
            Elf64_Sym sym;
            fseek(f, shdr.sh_offset + j * shdr.sh_entsize, SEEK_SET);
            if (fread(&sym, 1, sizeof(sym), f) != sizeof(sym)) break;
            if (sym.st_value == 0) continue;
            const char *sname = strtab_buf + sym.st_name;
            if (strcmp(sname, name) == 0) {
                void *result = (void *)(load_bias + sym.st_value);
                LOGI("libart_resolver: %s = %p (file .symtab, section %u)", name, result, i);
                free(strtab_buf);
                free(shstrtab);
                fclose(f);
                return result;
            }
        }
        free(strtab_buf);
    }
    free(shstrtab);
    fclose(f);
    LOGE("libart_resolver: %s NOT FOUND in .symtab either", name);
    return nullptr;
}

static void *libart_prefix_resolver(const char *prefix) {
    if (!g_libart_handle) {
        g_libart_handle = dlopen("libart.so", RTLD_NOLOAD);
    }
    uintptr_t base = find_libart_base();
    if (!base) return nullptr;
    auto *ehdr = (Elf64_Ehdr *)base;
    auto *phdr = (Elf64_Phdr *)(base + ehdr->e_phoff);
    const char *strtab = nullptr;
    Elf64_Sym *symtab = nullptr;
    uint32_t strsz = 0;
    for (int i = 0; i < ehdr->e_phnum; i++) {
        if (phdr[i].p_type == PT_DYNAMIC) {
            auto *dyn = (Elf64_Dyn *)(base + phdr[i].p_offset);
            for (Elf64_Dyn *d = dyn; d->d_tag != 0; d++) {
                if (d->d_tag == DT_STRTAB) { int64_t g = mem_segment_gap(base, d->d_un.d_ptr); strtab = (const char *)(base + d->d_un.d_ptr - g); }
                if (d->d_tag == DT_SYMTAB) { int64_t g = mem_segment_gap(base, d->d_un.d_ptr); symtab = (Elf64_Sym *)(base + d->d_un.d_ptr - g); }
                if (d->d_tag == DT_STRSZ) strsz = (uint32_t)d->d_un.d_val;
            }
            break;
        }
    }
    if (!strtab || !symtab) return nullptr;
    for (uint32_t i = 1; symtab[i].st_name < strsz; i++) {
        if (symtab[i].st_value == 0) continue;
        const char *sname = strtab + symtab[i].st_name;
        if (strncmp(sname, prefix, strlen(prefix)) == 0) {
            int64_t gap = mem_segment_gap(base, symtab[i].st_value);
            void *result = (void *)(base + symtab[i].st_value - gap);
            LOGI("libart_prefix_resolver: %s -> %s = %p", prefix, sname, result);
            return result;
        }
    }
    return nullptr;
}

static bool lsplant_init(JNIEnv *env) {
    if (g_lsplant_initialized) return true;
    LOGI("lsplant_init: START");
    lsplant::InitInfo info{};
    info.inline_hooker = [](void *target, void *hooker) -> void * {
        return inline_hooker(target, hooker);
    };
    info.inline_unhooker = [](void *func) -> bool {
        return inline_unhooker(func);
    };
    info.art_symbol_resolver = [](std::string_view name) -> void * {
        return libart_resolver(std::string(name).c_str());
    };
    info.art_symbol_prefix_resolver = [](std::string_view prefix) -> void * {
        return libart_prefix_resolver(std::string(prefix).c_str());
    };
    bool ok = lsplant::Init(env, info);
    LOGI("lsplant_init: result=%d", ok);
    g_lsplant_initialized = ok;
    return ok;
}

static jobject lsplant_hook_method(JNIEnv *env, jclass cls, const char *name, const char *sig,
                                    bool is_static, int hookId) {
    LOGI("lsplant_hook_method: %s hookId=%d", name, hookId);
    jmethodID mid = is_static ? env->GetStaticMethodID(cls, name, sig)
                               : env->GetMethodID(cls, name, sig);
    if (!mid) { env->ExceptionClear(); LOGE("lsplant_hook_method: %s not found", name); return nullptr; }
    jobject method = env->ToReflectedMethod(cls, mid, is_static);
    if (!method) { env->ExceptionClear(); LOGE("lsplant_hook_method: ToReflectedMethod failed for %s", name); return nullptr; }

    jobject callback_obj = env->NewObject(g_hooker_cls, g_hooker_ctor, hookId);
    if (!callback_obj || env->ExceptionCheck()) { env->ExceptionClear(); LOGE("lsplant_hook_method: NewObject failed"); return nullptr; }

    jobject backup = lsplant::Hook(env, method, callback_obj, g_hooker_callback_method);
    if (!backup) { env->ExceptionClear(); LOGE("lsplant_hook_method: Hook FAILED for %s", name); return nullptr; }

    jfieldID backup_fid = env->GetFieldID(g_hooker_cls, "backup", "Ljava/lang/reflect/Method;");
    env->SetObjectField(callback_obj, backup_fid, backup);
    LOGI("lsplant_hook_method: %s OK, backup=%p", name, backup);
    return backup;
}

// ==================== il2cpp API ====================

typedef void* (*fn_domain_get)();
typedef const void** (*fn_domain_get_assemblies)(void*, size_t*);
typedef const void* (*fn_assembly_get_image)(const void*);
typedef const void* (*fn_class_from_name)(const void*, const char*, const char*);
typedef const void* (*fn_class_get_methods)(const void*, void**);
typedef const char*  (*fn_method_get_name)(const void*);
// il2cpp_method_get_pointer removed — Il2CppMethodInfo starts with methodPointer at offset 0
typedef void*        (*fn_method_get_invoke_ptr)(const void*);

static void install_immortality(uintptr_t il2cpp_base) {
    LOGI("install_immortality: base=%p", (void *)il2cpp_base);
    auto f_domain    = (fn_domain_get)            elf_find_symbol(il2cpp_base, "il2cpp_domain_get");
    auto f_assemblies= (fn_domain_get_assemblies) elf_find_symbol(il2cpp_base, "il2cpp_domain_get_assemblies");
    auto f_image     = (fn_assembly_get_image)    elf_find_symbol(il2cpp_base, "il2cpp_assembly_get_image");
    auto f_fromname  = (fn_class_from_name)       elf_find_symbol(il2cpp_base, "il2cpp_class_from_name");
    auto f_methods   = (fn_class_get_methods)     elf_find_symbol(il2cpp_base, "il2cpp_class_get_methods");
    auto f_mname     = (fn_method_get_name)       elf_find_symbol(il2cpp_base, "il2cpp_method_get_name");
    // il2cpp_method_get_pointer not exported — read from Il2CppMethodInfo struct (offset 0 = methodPointer)
    LOGI("install_immortality: domain=%p asm=%p img=%p name=%p methods=%p mname=%p",
         (void*)f_domain, (void*)f_assemblies, (void*)f_image, (void*)f_fromname,
         (void*)f_methods, (void*)f_mname);
    if (!f_domain || !f_assemblies || !f_image || !f_fromname) {
        LOGE("il2cpp API not found via ELF");
        return;
    }
    if (!f_methods || !f_mname) {
        LOGE("il2cpp method API not found (methods=%p mname=%p)", (void*)f_methods, (void*)f_mname);
        return;
    }
    LOGI("il2cpp API found via ELF");
    void *dom = f_domain();
    LOGI("install_immortality: domain=%p", dom);
    size_t cnt = 0;
    const void **assemblies = f_assemblies(dom, &cnt);
    LOGI("install_immortality: %zu assemblies", cnt);
    for (size_t i = 0; i < cnt; i++) {
        const void *img = f_image(assemblies[i]);
        const void *kls = f_fromname(img, "", "Character");
        if (!kls) continue;
        LOGI("install_immortality: found Character in assembly %zu", i);
        void *iter = nullptr;
        const void *method;
        fop_fn orig = nullptr;
        while ((method = f_methods(kls, &iter)) != nullptr) {
            const char *mname = f_mname(method);
            if (mname && strcmp(mname, "fwa") == 0) {
                orig = (fop_fn)(*(void**)method);
                LOGI("install_immortality: found fwa method: name=%s ptr=%p", mname, (void*)orig);
                break;
            }
        }
        if (!orig) {
            LOGE("install_immortality: fwa method NOT FOUND via il2cpp_class_get_methods");
            LOGI("install_immortality: dumping all methods of Character:");
            iter = nullptr;
            int dump_count = 0;
            while ((method = f_methods(kls, &iter)) != nullptr && dump_count < 50) {
                const char *mname = f_mname(method);
                void *mptr = *(void**)method;
                LOGI("  method[%d]: %s ptr=%p", dump_count, mname ? mname : "(null)", mptr);
                dump_count++;
            }
            return;
        }
        if (!orig) {
            LOGE("install_immortality: fwa ptr is NULL");
            return;
        }
        void *trampoline = alloc_exec_near((void*)orig, 64);
        if (!trampoline) {
            LOGE("install_immortality: alloc_exec trampoline failed");
            return;
        }
        memcpy(trampoline, (void *)orig, 16);
        fix_adrp_in_trampoline(trampoline, (void*)orig);
        uint32_t *t = (uint32_t *)trampoline;
        t[4] = 0x58000050;
        t[5] = 0xD61F0200;
        *(uint64_t *)&t[6] = (uint64_t)orig + 16;
        __builtin___clear_cache((char *)trampoline, (char *)trampoline + 64);
        uint8_t jmp[16];
        *(uint32_t *)&jmp[0] = 0x58000050;
        *(uint32_t *)&jmp[4] = 0xD61F0200;
        *(uint64_t *)&jmp[8] = (uint64_t)my_fop;
        if (proc_mem_write((uintptr_t)orig, jmp, 16)) {
            __builtin___clear_cache((char *)orig, (char *)orig + 16);
            if (memcmp((void *)orig, jmp, 16) == 0) {
                g_orig_fop = (fop_fn)trampoline;
                LOGI("Immortality inline hook OK (func=%p, trampoline=%p)", (void *)orig, trampoline);
            } else {
                LOGE("Immortality inline hook VERIFY FAILED (func=%p) — patch did not stick", (void *)orig);
            }
        } else {
            LOGE("Immortality inline hook FAILED (proc_mem_write for %p)", (void *)orig);
        }
        return;
    }
    LOGE("DungeonRush.Character class not found");
}

typedef int (*proc_purchase_fn)(void *self, void *args);
static proc_purchase_fn g_orig_proc_purchase = nullptr;
static uintptr_t g_proc_purchase_entry = 0; // entry reale risolta via API il2cpp (da install_store_hook)
// v4.58: base REALE per gli indirizzi assoluti (file-VMA as objdump). Il vecchio uso
// di g_il2cpp_base+offset assumeva bias=VMA nel segmento .text, ma il segmento LOAD e'
// mappato con gap 0x4000 (vaddr=fileoff+0x4000): runtime = bias + VMA - 0x4000.
// Noto che g_proc_purchase_entry (indirizzo runtime) - RVA dell'entry nel file (v56: 0x28ACD34)
// da' esattamente la base tale che addr_assoluto = base + fileVMA. Tutti i probe assoluti
// (fallback, grant chain) devono usare g_code_base, NON g_il2cpp_base.
// g_code_base definito in alto (insieme ai probe user): addr_assoluto = g_code_base + RVA.
// = g_proc_purchase_entry - 0x28ACD34 (impostata in install_store_hook)
static bool my_dnf(void *self, void *args);
static void dump_handler_method_name(uintptr_t base);
static void dump_purchase_types(uintptr_t base);
static void dump_grant_bl(uintptr_t entry);
static bool safe_read_self(uintptr_t addr, void *buf, size_t len);

// API-based dump del metodo handler. [dw+0x50] è l'oggetto handler (l'oggetto
// su cui ProcessPurchase esegue il vtable lookup del metodo per nome). Usiamo
// l'API il2cpp per risolvere la classe dell'oggetto ed enumerare i metodi con
// il nome, invece di indovinare il layout di memoria. Questo dá il NOME reale
// del metodo handler, che è quello che serve per capire il grant.
// Risolve le funzioni una sola volta (static cache).
typedef const void* (*fn_object_get_class)(const void* obj);
typedef const char* (*fn_class_get_name)(const void* klass);
typedef const char* (*fn_class_get_namespace)(const void* klass);
static bool dump_handler_via_api(void *dw_self) {
    if (!dw_self) return false;
    static void *f_objclass = nullptr, *f_cname = nullptr, *f_cns = nullptr;
    static void *f_methods = nullptr, *f_mname = nullptr;
    if (!f_objclass && g_il2cpp_base) {
        f_objclass = elf_find_symbol(g_il2cpp_base, "il2cpp_object_get_class");
        f_cname    = elf_find_symbol(g_il2cpp_base, "il2cpp_class_get_name");
        f_cns      = elf_find_symbol(g_il2cpp_base, "il2cpp_class_get_namespace");
        f_methods  = elf_find_symbol(g_il2cpp_base, "il2cpp_class_get_methods");
        f_mname    = elf_find_symbol(g_il2cpp_base, "il2cpp_method_get_name");
        LOGI("API_DIAG: objclass=%p cname=%p cns=%p methods=%p mname=%p",
             f_objclass, f_cname, f_cns, f_methods, f_mname);
    }
    if (!f_objclass || !f_cname || !f_cns || !f_methods || !f_mname) {
        LOGE("API_DIAG: il2cpp API symbols not found");
        return false;
    }
    // [dw+0x50] = oggetto handler
    uint64_t handler = 0;
    if (!safe_read_self((uintptr_t)dw_self + 0x50, &handler, 8)) {
        LOGI("API_DIAG: read dw+0x50 FAIL (self=%p)", dw_self);
        return false;
    }
    LOGI("API_DIAG: [dw+0x50] handler obj = 0x%llx", (unsigned long long)handler);
    if (!handler) { LOGI("API_DIAG: handler null"); return false; }
    const void *klass = ((fn_object_get_class)f_objclass)((void*)handler);
    LOGI("API_DIAG: handler class = %p", klass);
    if (!klass) return false;
    const char *cname = ((fn_class_get_name)f_cname)(klass);
    const char *cns   = ((fn_class_get_namespace)f_cns)(klass);
    LOGI("API_DIAG: handler class name='%s' namespace='%s'", cname ? cname : "(null)", cns ? cns : "(null)");
    // Enumeriamo i metodi della classe (come fa install_immortality per Character)
    void *iter = nullptr;
    const void *method;
    int cnt = 0;
    while ((method = ((fn_class_get_methods)f_methods)(klass, &iter)) != nullptr && cnt < 100) {
        const char *mname = ((fn_method_get_name)f_mname)(method);
        LOGI("API_DIAG:   method[%d] '%s'", cnt, mname ? mname : "(null)");
        cnt++;
    }
    LOGI("API_DIAG: %d methods enumerated", cnt);
    return true;
}

static int my_proc_purchase(void *self, void *args) {
    LOGI("STORE_STEP: my_proc_purchase ENTRY called by %p self=%p args=%p", __builtin_return_address(0), self, args);
    static bool s_diag_dumped = false;
    if (!s_diag_dumped && g_proc_purchase_entry) {
        s_diag_dumped = true;
        uintptr_t base = g_proc_purchase_entry - 0x28ACD34;
        LOGI("PROC_PURCH_DIAG: first call — dumping handler via il2cpp API (base=%p)", (void*)base);
        dump_handler_via_api(self);
        dump_handler_method_name(base);
        dump_purchase_types(base);
        dump_grant_bl(g_proc_purchase_entry);
    }
    LOGI("STORE_STEP: my_proc_purchase: g_orig_proc_purchase=%p (trampoline), will CALL ORIGINAL so grant chain runs; my_dnf(eb.dnf) hooked to return true for verification", (void*)g_orig_proc_purchase);
    LOGI("STORE_STEP: my_proc_purchase: self eb=%p, args PurchaseEventArgs=%p, caller=%p, next expected HOOK is my_dnf (eb.dnf) at %p", self, args, __builtin_return_address(0), (void*)my_dnf);
    static bool s_state_dumped = false;
    if (!s_state_dumped) {
        s_state_dumped = true;
        uintptr_t ab = (uintptr_t)args, sb = (uintptr_t)self;
        uint64_t a8=0,a10=0,a18=0, h50=0, p30=0;
        if (ab) { safe_read_self(ab+0x08,&a8,8); safe_read_self(ab+0x10,&a10,8); safe_read_self(ab+0x18,&a18,8); }
        if (sb) { safe_read_self(sb+0x50,&h50,8); safe_read_self(sb+0x30,&p30,8); }
        LOGI("STORE_STEP: my_proc_purchase state: args+0x8=%p args+0x10=%p args+0x18=%p | eb+0x50(handler)=%p eb+0x30(product)=%p",
             (void*)a8,(void*)a10,(void*)a18,(void*)h50,(void*)p30);
    }
    int ret = 0;
    if (g_orig_proc_purchase) {
        ret = g_orig_proc_purchase(self, args);
        LOGI("STORE_STEP: my_proc_purchase called ORIGINAL -> returned %d (dnf hooked true => grant path)", ret);
    } else {
        LOGI("STORE_STEP: my_proc_purchase NO original, forcing Complete(0)");
    }
    LOGI("STORE_STEP: my_proc_purchase EXIT returning %d -> if my_dnf triggered then verification passed and grant should follow", ret);
    return ret;
}
static void install_store_hook(uintptr_t il2cpp_base) {
    LOGI("install_store_hook: base=%p (near alloc for adrp)", (void *)il2cpp_base);
    auto f_domain    = (fn_domain_get)            elf_find_symbol(il2cpp_base, "il2cpp_domain_get");
    auto f_assemblies= (fn_domain_get_assemblies) elf_find_symbol(il2cpp_base, "il2cpp_domain_get_assemblies");
    auto f_image     = (fn_assembly_get_image)    elf_find_symbol(il2cpp_base, "il2cpp_assembly_get_image");
    auto f_fromname  = (fn_class_from_name)       elf_find_symbol(il2cpp_base, "il2cpp_class_from_name");
    auto f_methods   = (fn_class_get_methods)     elf_find_symbol(il2cpp_base, "il2cpp_class_get_methods");
    auto f_mname     = (fn_method_get_name)       elf_find_symbol(il2cpp_base, "il2cpp_method_get_name");
    if (!f_domain || !f_assemblies || !f_image || !f_fromname || !f_methods || !f_mname) { LOGE("install_store_hook: il2cpp API not found"); return; }
    void *dom = f_domain();
    size_t cnt = 0;
    const void **assemblies = f_assemblies(dom, &cnt);
    for (size_t i = 0; i < cnt; i++) {
        const void *img = f_image(assemblies[i]);
        const void *kls = f_fromname(img, "", "eb");
        if (!kls) continue;
        LOGI("install_store_hook: found eb in assembly %zu", i);
        void *iter = nullptr;
        const void *method;
        proc_purchase_fn orig = nullptr;
        while ((method = f_methods(kls, &iter)) != nullptr) {
            const char *mname = f_mname(method);
            if (mname && strcmp(mname, "ProcessPurchase") == 0) {
                orig = (proc_purchase_fn)(*(void**)method);
                LOGI("install_store_hook: found ProcessPurchase ptr=%p", (void*)orig);
                break;
            }
        }
        if (!orig) { LOGE("install_store_hook: ProcessPurchase not found"); return; }
        g_proc_purchase_entry = (uintptr_t)orig;
        g_code_base = g_proc_purchase_entry - 0x28ACD34;   // base file-VMA reale (v4.58 fix, v56 eb.ProcessPurchase)
        LOGI("install_store_hook: entry reale ProcessPurchase=%p (per probe ptr+offset)", (void*)g_proc_purchase_entry);
        LOGI("install_store_hook: code_base=%p (entry - 0x28ACD34) — probe assoluti useranno questa base, NON g_il2cpp_base", (void*)g_code_base);
        // Hook at entry (with adrp fixup) to avoid stack leak from +0x1C hook
        void *hook_target = (void*)orig;
        {
            uint8_t *b = (uint8_t*)hook_target;
            LOGI("install_store_hook: orig bytes @%p: %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x",
                 orig, b[0],b[1],b[2],b[3],b[4],b[5],b[6],b[7],b[8],b[9],b[10],b[11],b[12],b[13],b[14],b[15]);
            for(int i=0;i<16;i+=4){
                uint32_t ins=*(uint32_t*)(b+i);
                if((ins&0x9F000000)==0x90000000) LOGI("  adrp @+%d: %08x", i, ins);
            }
        }
        void *trampoline = alloc_exec_near(hook_target, 64);
        if (!trampoline) { LOGE("install_store_hook: alloc_exec failed"); return; }
        memcpy(trampoline, hook_target, 16);
        fix_adrp_in_trampoline(trampoline, hook_target);
        uint32_t *t2 = (uint32_t*)trampoline; t2[4]=0x58000050; t2[5]=0xD61F0200; *(uint64_t*)&t2[6]=(uint64_t)orig+16;
        __builtin___clear_cache((char*)trampoline, (char*)trampoline+64);
        uint8_t jmp[16];
        *(uint32_t*)&jmp[0] = 0x58000050; // ldr x16, #8
        *(uint32_t*)&jmp[4] = 0xD61F0200; // br x16
        *(uint64_t*)&jmp[8] = (uint64_t)my_proc_purchase;
        LOGI("Store hook: patching %p with ldr/br 16B -> %p", orig, my_proc_purchase);
        if (proc_mem_write((uintptr_t)hook_target, jmp, 16)) {
            __builtin___clear_cache((char *)hook_target, (char *)hook_target + 16);
            if (memcmp(hook_target, jmp, 16)==0) {
                g_orig_proc_purchase = (proc_purchase_fn)trampoline;
                LOGI("Store hook OK (func=%p entry 16B, trampoline=%p)", (void *)orig, trampoline);
            } else LOGE("Store hook VERIFY FAILED");
        } else LOGE("Store hook FAILED");
        return;
    }
    LOGE("eb class not found");
}
typedef bool (*dnf_fn)(void *self, void *args);
static dnf_fn g_orig_dnf = nullptr;
static bool my_dnf(void *self, void *args) {
    LOGI("STORE_STEP: my_dnf ENTRY called by %p self=%p dw:%p args=%p PurchaseEventArgs %p", __builtin_return_address(0), self, self, args, args);
    LOGI("STORE_STEP: my_dnf: g_orig_dnf=%p, will BYPASS verification and return true (grant inventory next)", (void*)g_orig_dnf);
    LOGI("STORE_STEP: my_dnf: caller stack 0x%p -> 0x%p, next expected: inventory grant (Unity Purchasing StoreListenerProxy.onPurchaseSucceeded)", __builtin_return_address(1), __builtin_return_address(0));
    bool ret = true;
    LOGI("STORE_STEP: my_dnf EXIT returning %d (true=verified) -> game should call OnPurchaseSucceeded/ConfirmPendingPurchase next", ret);
    return ret;
}
static void install_dnf_hook(uintptr_t il2cpp_base) {
    LOGI("install_dnf_hook: base=%p", (void *)il2cpp_base);
    auto f_domain    = (fn_domain_get)            elf_find_symbol(il2cpp_base, "il2cpp_domain_get");
    auto f_assemblies= (fn_domain_get_assemblies) elf_find_symbol(il2cpp_base, "il2cpp_domain_get_assemblies");
    auto f_image     = (fn_assembly_get_image)    elf_find_symbol(il2cpp_base, "il2cpp_assembly_get_image");
    auto f_fromname  = (fn_class_from_name)       elf_find_symbol(il2cpp_base, "il2cpp_class_from_name");
    auto f_methods   = (fn_class_get_methods)     elf_find_symbol(il2cpp_base, "il2cpp_class_get_methods");
    auto f_mname     = (fn_method_get_name)       elf_find_symbol(il2cpp_base, "il2cpp_method_get_name");
    if (!f_domain || !f_assemblies || !f_image || !f_fromname || !f_methods || !f_mname) return;
    void *dom = f_domain(); size_t cnt = 0; const void **assemblies = f_assemblies(dom, &cnt);
    for (size_t i = 0; i < cnt; i++) {
        const void *img = f_image(assemblies[i]);
        const void *kls = f_fromname(img, "", "eb");
        if (!kls) continue;
        void *iter = nullptr; const void *method;
        while ((method = f_methods(kls, &iter)) != nullptr) {
            const char *mname = f_mname(method);
            if (mname && strcmp(mname, "dnf") == 0) {
                dnf_fn orig = (dnf_fn)(*(void**)method);
                LOGI("install_dnf_hook: found dnf ptr=%p", (void*)orig);
                {
                    uint8_t *b = (uint8_t*)orig;
                    LOGI("install_dnf_hook: orig bytes @%p: %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x",
                         orig, b[0],b[1],b[2],b[3],b[4],b[5],b[6],b[7],b[8],b[9],b[10],b[11],b[12],b[13],b[14],b[15]);
                    for(int i=0;i<16;i+=4){
                        uint32_t ins=*(uint32_t*)(b+i);
                        if((ins&0x9F000000)==0x90000000) LOGI("  adrp @+%d: %08x", i, ins);
                    }
                }
                void *tramp = alloc_exec_near((void*)orig, 64);
                if (!tramp) return;
                memcpy(tramp, (void*)orig, 16);
                fix_adrp_in_trampoline(tramp, (void*)orig);
                uint32_t *t = (uint32_t*)tramp; t[4]=0x58000050; t[5]=0xD61F0200; *(uint64_t*)&t[6]=(uint64_t)orig+16;
                __builtin___clear_cache((char*)tramp, (char*)tramp+64);
                uint8_t jmp[16]; *(uint32_t*)&jmp[0]=0x58000050; *(uint32_t*)&jmp[4]=0xD61F0200; *(uint64_t*)&jmp[8]=(uint64_t)my_dnf;
                if (proc_mem_write((uintptr_t)orig, jmp, 16)) {
                    __builtin___clear_cache((char*)orig, (char*)orig+16);
                    if (memcmp((void*)orig, jmp, 16)==0) { g_orig_dnf = (dnf_fn)tramp; LOGI("dnf hook OK"); } else LOGE("dnf hook verify fail");
                }
                return;
            }
        }
    }
}

typedef void* (*fn_runtime_invoke)(const void* method, void* obj, void** params, void** exc);
static fn_runtime_invoke g_orig_runtime_invoke = nullptr;
static fn_method_get_name g_runtime_mname = nullptr;
static thread_local bool g_in_runtime_hook = false;
static void* my_runtime_invoke_correct(const void* method, void* obj, void** params, void** exc) {
    if (g_in_runtime_hook) return g_orig_runtime_invoke ? g_orig_runtime_invoke(method, obj, params, exc) : nullptr;
    const char* mname = g_runtime_mname ? g_runtime_mname(method) : nullptr;
    bool is_purchase = mname && (strstr(mname, "Purchase") || strstr(mname, "dnf") || strstr(mname, "Confirm") || strstr(mname, "ProcessPurchase"));
    if (is_purchase) {
        g_in_runtime_hook = true;
        LOGI("REAL_CALL: >>> %s obj=%p caller=%p", mname, obj, __builtin_return_address(0));
        g_in_runtime_hook = false;
    }
    g_in_runtime_hook = true;
    void* ret = g_orig_runtime_invoke ? g_orig_runtime_invoke(method, obj, params, exc) : nullptr;
    g_in_runtime_hook = false;
    if (is_purchase) {
        g_in_runtime_hook = true;
        LOGI("REAL_CALL: <<< %s returned %p", mname, ret);
        g_in_runtime_hook = false;
    }
    return ret;
}
static void install_runtime_invoke_hook_correct(uintptr_t base) {
    auto f_runtime = (fn_runtime_invoke)elf_find_symbol(base, "il2cpp_runtime_invoke");
    auto f_mname2 = (fn_method_get_name)elf_find_symbol(base, "il2cpp_method_get_name");
    if (!f_runtime || !f_mname2) { LOGE("runtime_invoke symbols not found"); return; }
    g_runtime_mname = f_mname2;
    LOGI("install_runtime_invoke_correct: il2cpp_runtime_invoke at %p", (void*)f_runtime);
    void *tramp = alloc_exec_near((void*)f_runtime, 64);
    if (!tramp) return;
    memcpy(tramp, (void*)f_runtime, 16);
    fix_adrp_in_trampoline(tramp, (void*)f_runtime);
    uint32_t *t = (uint32_t*)tramp; t[4]=0x58000050; t[5]=0xD61F0200; *(uint64_t*)&t[6]=(uint64_t)f_runtime+16;
    __builtin___clear_cache((char*)tramp, (char*)tramp+64);
    uint8_t jmp[16]; *(uint32_t*)&jmp[0]=0x58000050; *(uint32_t*)&jmp[4]=0xD61F0200; *(uint64_t*)&jmp[8]=(uint64_t)my_runtime_invoke_correct;
    if (proc_mem_write((uintptr_t)f_runtime, jmp, 16)) {
        __builtin___clear_cache((char*)f_runtime, (char*)f_runtime+16);
        if (memcmp((void*)f_runtime, jmp, 16)==0) { g_orig_runtime_invoke = (fn_runtime_invoke)tramp; LOGI("runtime_invoke correct hook OK"); }
    }
}

// REAL_CALL hook - corretto con guard e log reale del next
// Store real next - targeted dw hooks using known working inline_hooker (no runtime_invoke)
static void common_dw_logger(const char* name, void* caller) {
    LOGI("REAL_CALL: >>> dw.%s caller=%p", name, caller);
}
typedef bool (*dnf_real_fn)(void* self, void* args);
typedef int (*proc_real_fn)(void* self, void* args);
static dnf_real_fn g_orig_dnf_real = nullptr;
static proc_real_fn g_orig_proc_real = nullptr;
static bool my_dnf_real(void* self, void* args) {
    LOGI("REAL_CALL: >>> dw.dnf self=%p args=%p caller=%p", self, args, __builtin_return_address(0));
    bool r = g_orig_dnf_real ? g_orig_dnf_real(self, args) : true;
    LOGI("REAL_CALL: <<< dw.dnf returned %d", r);
    return r;
}
static int my_proc_real(void* self, void* args) {
    LOGI("REAL_CALL: >>> dw.ProcessPurchase self=%p args=%p caller=%p", self, args, __builtin_return_address(0));
    int r = g_orig_proc_real ? g_orig_proc_real(self, args) : 0;
    LOGI("REAL_CALL: <<< dw.ProcessPurchase returned %d", r);
    return r;
}
static void install_dw_store_real_hooks(uintptr_t base) {
    LOGI("install_dw_store_real_hooks: base=%p", (void*)base);
    auto f_domain = (fn_domain_get)elf_find_symbol(base, "il2cpp_domain_get");
    auto f_assemblies = (fn_domain_get_assemblies)elf_find_symbol(base, "il2cpp_domain_get_assemblies");
    auto f_image = (fn_assembly_get_image)elf_find_symbol(base, "il2cpp_assembly_get_image");
    auto f_fromname = (fn_class_from_name)elf_find_symbol(base, "il2cpp_class_from_name");
    auto f_methods = (fn_class_get_methods)elf_find_symbol(base, "il2cpp_class_get_methods");
    auto f_mname = (fn_method_get_name)elf_find_symbol(base, "il2cpp_method_get_name");
    if (!f_domain || !f_assemblies || !f_image || !f_fromname || !f_methods || !f_mname) { LOGE("dw real hooks: symbols not found"); return; }
    void* dom = f_domain(); size_t cnt=0; const void** ass = f_assemblies(dom,&cnt);
    for (size_t i=0;i<cnt;i++) {
        const void* img = f_image(ass[i]);
        const void* kls = f_fromname(img, "", "eb");
        if (!kls) continue;
        LOGI("install_dw_store_real_hooks: found eb class in assembly %zu", i);
        void* iter=nullptr; const void* m;
        while ((m=f_methods(kls,&iter))!=nullptr) {
            const char* n = f_mname(m);
            if (!n) continue;
            void* orig = *(void**)m;
            // Hook every dw method for REAL_CALL (store only is still dw, but we log all to see next)
            LOGI("install_dw_store_real_hooks: hooking dw.%s at %p", n, orig);
            // Create trampoline for original
            void* tramp = alloc_exec_near(orig, 64);
            if (!tramp) continue;
            memcpy(tramp, orig, 16);
            fix_adrp_in_trampoline(tramp, orig);
            uint32_t *t=(uint32_t*)tramp; t[4]=0x58000050; t[5]=0xD61F0200; *(uint64_t*)&t[6]=(uint64_t)orig+16;
            __builtin___clear_cache((char*)tramp,(char*)tramp+64);
            // Create stub that logs method name and jumps to trampoline (for REAL_CALL) or to my_dnf/my_proc for specific
            void* hook_target = nullptr;
            if (strcmp(n, "dnf")==0) hook_target = (void*)my_dnf_real;
            else if (strcmp(n, "ProcessPurchase")==0) hook_target = (void*)my_proc_real;
            else {
                // Generic stub for other dw methods: log via common_dw_logger then jump to trampoline
                char* name_copy = (char*)malloc(strlen(n)+1);
                strcpy(name_copy, n);
                // Allocate stub: save x30, call logger, restore, jump to trampoline
                uint8_t* stub = (uint8_t*)alloc_exec_near(orig, 96);
                if (!stub) continue;
                // Stub assembly: stp x29,x30,[sp,#-16]!; mov x29, sp; adrp x0, #page; add x0,x0,#off; bl common_dw_logger; ldp x29,x30,[sp],#16; ldr x16, #8; br x16; .quad trampoline; .quad name_copy
                // Simplified: just use inline_hooker with my generic that logs via common_dw_logger
                // For minimal, just hook with a small trampoline that logs name via common_dw_logger
                // Use existing my_dnf_real/my_proc_real for those, for others use a generic that logs name
                // Create a tiny hook that calls common_dw_logger then jumps to trampoline
                // For now, create a hook that is the same as my_dnf_real but with name
                // To avoid complexity, just hook with my_dnf_real for all (it will log as dnf, but we need correct name)
                // Instead, create a per-method stub that loads name and calls common_dw_logger
                // Allocate stub
                uintptr_t stub_addr = (uintptr_t)stub;
                uintptr_t name_addr = (uintptr_t)name_copy;
                uintptr_t logger_addr = (uintptr_t)common_dw_logger;
                uintptr_t tramp_addr = (uintptr_t)tramp;
                // Stub code (ARM64):
                // 0x00: stp x29,x30,[sp,#-16]! (FD 7B BF A9)
                // 0x04: mov x29, sp (FD 03 00 91)
                // 0x08: ldr x0, #16 (00 00 00 58)
                // 0x0C: ldr x16, #16 (50 00 00 58) ; actually need to load logger addr
                // 0x10: blr x16
                // 0x14: ldp x29,x30,[sp],#16
                // 0x18: ldr x16, #8
                // 0x1C: br x16
                // 0x20: .quad name_copy
                // 0x28: .quad logger
                // 0x30: .quad tramp
                // Simplified: just log via LOGI directly in stub is complex, so for now just use my_dnf_real as generic for all (will log as dnf, but we will see at least that something was called)
                hook_target = (void*)my_dnf_real;
                // Keep name_copy for debugging but not used in this minimal stub
                (void)name_copy;
            }
            uint8_t jmp[16]; *(uint32_t*)&jmp[0]=0x58000050; *(uint32_t*)&jmp[4]=0xD61F0200; *(uint64_t*)&jmp[8]=(uint64_t)hook_target;
            if (proc_mem_write((uintptr_t)orig, jmp, 16)) {
                __builtin___clear_cache((char*)orig,(char*)orig+16);
                if (memcmp(orig,jmp,16)==0) {
                    if (strcmp(n, "dnf")==0) g_orig_dnf_real=(dnf_real_fn)tramp;
                    else if (strcmp(n, "ProcessPurchase")==0) g_orig_proc_real=(proc_real_fn)tramp;
                    LOGI("dw.%s real hook OK trampoline=%p", n, tramp);
                }
            }
        }
        break;
    }
}

// ==================== ProcessPurchase path fix (v56 eb.ProcessPurchase 0x28ACD34) ====================
// v56 ProcessPurchase (0x28acd34): il flusso ha DUE gate/decisioni senza bl separato:
//   GATE1 @ entry+0x118 (0x28ACE4C): tbz w0, #0, 0x28ACEcc
//     bit0==0 → salto a 0x28ACEcc = path FALSE: check receipt, poi GATE2 -> UserController.eao
//            (0x2991924) che FA SOLO ENQUEUE su User.PendingPurchases (count++, nessun reward).
//     bit0==1 → fall-through = path TRUE (0x28ACE50): NOP => invoca l'evento Action<Product>
//            a [0x5a62000+0x9c8] con x1=product ([x20+0x10]) e ritorna 1. QUESTO E' IL GRANT:
//            l'handler registrato dal gioco applica la ricompensa (v56, verificato da dump.cs
//            + disassembly libil2cpp.so).
//   GATE2 @ entry+0x1D0 (0x28ACF04): tbz w21,#0, 0x28ACF40 (solo sul path FALSE)
//     bit0==0 (stringa non vuota) → salto a 0x28ACF40 = GRANT PATH (bl 0x2991924 eao + dne)
//     bit0==1 (stringa vuota) → fall-through → return 0 (Pending)
// v4.75: GATE1 forzato a b pathFALSE => tutto il flusso finiva su eao/enqueue (count bloccato a 8,
// nessun reward). v4.76: GATE1 -> NOP => il flusso cade SEMPRE nel path TRUE che spara l'evento
// Action<Product> sul quale il gioco ha registrato l'handler di erogazione delle ricompense.
// NOP = patch piu' sicura possibile (nessun registro, nessun salto, 1 sola istruzione).
static void install_tbz_force_grant(void) {
    if (!g_proc_purchase_entry) { LOGE("tbz fix: g_proc_purchase_entry not set (store hook failed)"); return; }
    uintptr_t target = g_proc_purchase_entry + 0x118;       // GATE1 tbz w0,#0 (v56 0x28ACE4C)
    // Verify original: tbz w0,#0,<+0x80> = 0x36000400 (v56, verificato objdump)
    uint32_t orig = 0;
    if (safe_read_self(target, &orig, 4)) {
        LOGI("tbz fix: original @entry+0x118 = %08x", orig);
        if ((orig & 0x7F000000) != 0x36000000) {   // tbz Xt,#uimm5
            LOGE("tbz fix: UNEXPECTED bytes %08x (expected tbz w0,#0) — aborting", orig);
            return;
        }
    }
    uint32_t nop = 0xD503201F;                                  // NOP
    if (proc_mem_write(target, &nop, 4)) {
        __builtin___clear_cache((char*)target, (char*)target + 4);
        uint32_t verify = 0;
        safe_read_self(target, &verify, 4);
        LOGI("tbz fix: patched tbz@entry+0x118 -> NOP (fall-through a path TRUE: evento Action<Product> = GRANT)");
        LOGI("tbz fix: verify readback = %08x (expect D503201F)", verify);
    } else {
        LOGE("tbz fix: proc_mem_write FAILED at entry+0x118");
    }
}

// ==================== v56: GATE2 FALLBACK→GRANT force (entry+0x1D0) ====================
// In v56 ProcessPurchase (0x28acd34) il GATE2 (entry+0x1D0 = 0x28ACF04) testa il bit0 di w21,
// risultato di string.IsNullOrEmpty([receipt+0x40]):
//   bit0==0 → GRANT PATH (entry+0x20C = 0x28ACF40) → bl 0x2991924 (UserController.eao) + bl 0x28ACAC4 (dne)
//   bit0==1 → return 0 (Pending, nessuna erogazione)
// v4.76: GATE2 è raggiungibile SOLO dal path FALSE di GATE1. Con GATE1=NOP il flusso scende
// sempre dal path TRUE (evento Action<Product> = GRANT) e GATE2 NON viene mai eseguito.
// La funzione resta DEFINITA ma passa a NO-OP: LASCIA INTATTI i byte originali di entry+0x1D0
// (tbz w21,#0,0x28ACF40 = 0x360001F5) per non toccare path morto. Il CS_TABLE su 0x28acf04
// viene mantenuto: con i byte originali (0x36...) il cs_kind_ok(CS_B) fallisce → SKIP (nessun log
// ingannevole di "GRANT_PATH" su un percorso non più raggiungibile).
static void install_fallback_grant_force(void) {
    if (!g_proc_purchase_entry) { LOGE("fallback grant: g_proc_purchase_entry not set"); return; }
    uintptr_t target = g_proc_purchase_entry + 0x1D0;       // GATE2 tbz w21,#0 (v56 0x28ACF04)
    uint32_t orig = 0;
    if (safe_read_self(target, &orig, 4)) {
        LOGI("fallback grant: v4.76 NO-OP — GATE2 (%p) NON raggiungibile (GATE1=NOP -> path TRUE). byte originali intatti = %08x",
             (void *)target, orig);
    } else {
        LOGE("fallback grant: safe_read FAILED");
    }
}

// ==================== v4.77: dsq -> dsu redirect (grant COMPLETO al posto del wrapper vuoto) ====================
// In v56 il path TRUE di ProcessPurchase (con GATE1=NOP) scatta l'evento Action<Product> a
// [0x5a62000+0x9c8] il cui handler registrato dal gioco e' PurchaseController.dsq (privato,
// 0x28B68A4; la blr x9 @ 0x28ACEC4 lo evoca col delegate da [x8+0xb8]+0x30).
// dsq (0x28B68A4-0x28B6920) NON esegue il grant: azzera xfq/xfr, poi guarda [UserController+0xb8]+0x38
// (continuation registration); se non-null la esegue tail-call, altrimenti ritorna SENZA erogare
// -> resta solo il PendingIAPPopup ("purchase is pending approval") senza reward.
// dsu (0x28B6924, pubblico, void dsu(Product)) e' il grant VERO: chiude il PendingIAPPopup se attivo
// (0x28b6a28-0x28b6aa0) e poi applica le ricompense via lo switch su productId.
// STESSA FIRMA di dsq (this=x0, product=x1) => redirect: patch 4 byte su dsq entry con `b dsu`.
//   distanza = 0x28B6924 - 0x28B68A4 = 0x80 -> imm26 = 0x80>>2 = 0x20 -> encoding 0x14000020
//   VERIFICATO con clang/objdump (9k): `14000020 b 0x80 <tgt>` (vedi test in AppData/Temp/opencode/bcheck).
static void install_dsq_redirect(void) {
    if (!g_proc_purchase_entry) { LOGE("dsq redirect: g_proc_purchase_entry not set (store hook failed)"); return; }
    uintptr_t target = g_proc_purchase_entry + 0x9B70;       // RVA dsq 0x28B68A4 = entry + (0x28B68A4-0x28ACD34)
    // Verify original: stp x30,x21,[sp,#-0x20]! = 0xA9BE57FE (v56, verificato objdump)
    uint32_t orig = 0;
    if (safe_read_self(target, &orig, 4)) {
        LOGI("dsq redirect: original @dsq entry (RVA 0x28B68A4) = %08x", orig);
        if (orig != 0xA9BE57FE) {
            LOGE("dsq redirect: UNEXPECTED bytes %08x (expected a9be57fe) — aborting", orig);
            return;
        }
    } else {
        LOGE("dsq redirect: safe_read FAILED");
        return;
    }
    uint32_t branch = 0x14000020;                            // b 0x28B6924 (dsu)
    if (proc_mem_write(target, &branch, 4)) {
        __builtin___clear_cache((char*)target, (char*)target + 4);
        uint32_t verify = 0;
        safe_read_self(target, &verify, 4);
        LOGI("dsq redirect: patched dsq@RVA 0x28B68A4 -> b dsu (0x14000020): la callback del path TRUE ora esegue il GRANT COMPLETO");
        LOGI("dsq redirect: verify readback = %08x (expect 14000020)", verify);
    } else {
        LOGE("dsq redirect: proc_mem_write FAILED at dsq entry");
    }
}

// ==================== v4.71: STORE CHAIN FULL TRACE (log OGNI movimento) ====================
// Logga in tempo reale OGNI chiamata dentro la catena store v56:
//   eb.ProcessPurchase (0x28ACD34) -> GATE1/GATE2 forzati -> corridor -> bl 0x2991924 (eao)
//   -> tail-call b 0x29905D8 (flush); e bl 0x28ACAC4 (dne).
// Ogni call-site (bl), tail-jump (b), chiamata indiretta (blr/br) e gate cbz viene intercettato
// con uno stub full-preserve DEDICATO che logga caller+callee+argomenti, poi riesegue la
// semantica originale (la catena gira identica). Encodings ARM64 VERIFICATI con clang/objdump (9k):
//   ldr literal: 0x58000000|(imm19<<5)|Rt   mov x7,xN: 0xAA0003E7|(N<<16)
//   cbz: 0xB4000000|(imm19<<5)|Rt           b: 0x14000000|imm26
//   br xN: 0xD61F0000|(N<<5)                blr x16: 0xD63F0200
#define CS_BL   0   // bl call: set x30=site+4, br target
#define CS_B   1   // b tail: NO x30, br target
#define CS_BLR  2   // blr call: set x30=site+4, br reg (reg in d->reg) - continuazione a site+4
#define CS_CBZ  3   // gate cbz: log valore reg, ri-esegue cbz (tgt=EXIT) oppure b site+4
#define CS_BR   4   // br tail: NO x30, br reg (reg in d->reg) - x30 gia' valido del chiamante

struct CsSite {
    uint32_t site;      // RVA call-site (VMA file objdump)
    uint32_t tgt;       // RVA target (per CBZ: RVA del salto-EXIT)
    uint32_t kind;      // CS_BL / CS_B / CS_BLR / CS_CBZ
    uint32_t reg;       // registro testato/indiretto (31 = n/a -> mov x7,xzr)
    const char *caller;
    const char *callee;
};

static const struct CsSite CS_TABLE[] = {
    // ===================== eb.ProcessPurchase (0x28ACD34) =====================
    {0x28acd64,0x2674828,CS_BL,31, "eb.ProcessPurchase","init-helper 0x2674828"},
    {0x28acd70,0x2674828,CS_BL,31, "eb.ProcessPurchase","init-helper 0x2674828"},
    {0x28acd7c,0x2674828,CS_BL,31, "eb.ProcessPurchase","init-helper 0x2674828"},
    {0x28acd88,0x2674828,CS_BL,31, "eb.ProcessPurchase","init-helper 0x2674828"},
    {0x28acd94,0x2674828,CS_BL,31, "eb.ProcessPurchase","init-helper 0x2674828"},
    {0x28acda0,0x2674828,CS_BL,31, "eb.ProcessPurchase","init-helper 0x2674828"},
    {0x28acdac,0x2674828,CS_BL,31, "eb.ProcessPurchase","init-helper 0x2674828"},
    {0x28acdbc,0x2674a54,CS_BL,31, "eb.ProcessPurchase","alloc-obj 0x2674A54"},
    {0x28acdc8,0x4a9bf94,CS_BL,31, "eb.ProcessPurchase","shared-return 0x4A9BF94"},
    {0x28acdcc,0x28ad00c,CS_CBZ,21,"eb.ProcessPurchase","GATE x21(Param1)==null -> EXIT"},
    {0x28acdd0,0x28ad00c,CS_CBZ,20,"eb.ProcessPurchase","GATE x20(evt)==null -> EXIT"},
    {0x28acde0,0x28ad00c,CS_CBZ,22,"eb.ProcessPurchase","GATE x22(handler)==null -> EXIT"},
    {0x28ace24,0x26d5014,CS_BL,31, "eb.ProcessPurchase","resolve 0x26D5014"},
    {0x28ace48,0,CS_BLR,8,  "eb.ProcessPurchase","INDIRECT blr x8 (handler evento)"},
    // GATE1@0x28ACE4C: NOP permanente v4.76 (install_tbz_force_grant) -> nessun probe CS qui.
    {0x28ace64,0x2674940,CS_BL,31, "eb.ProcessPurchase","log-guard 0x2674940"},
    {0x28ace6c,0x2c3a650,CS_BL,31, "eb.ProcessPurchase","buf-alloc 0x2C3A650"},
    {0x28ace8c,0x2674940,CS_BL,31, "eb.ProcessPurchase","log-guard 0x2674940"},
    {0x28ace98,0x5462c30,CS_BL,31, "eb.ProcessPurchase","str? 0x5462C30"},
    {0x28acec4,0,CS_BLR,9,  "eb.ProcessPurchase","INDIRECT blr x9 (callback)"},
    {0x28acec8,0x28acff8,CS_B,31, "eb.ProcessPurchase","TRUE-path -> b ret1"},
    {0x28aced0,0x28ad00c,CS_CBZ,8, "eb.ProcessPurchase","GATE x8==null -> EXIT (niente grant)"},
    {0x28acedc,0x48e2104,CS_BL,31, "eb.ProcessPurchase","string.IsNullOrEmpty 0x48E2104"},
    {0x28acefc,0x2674940,CS_BL,31, "eb.ProcessPurchase","log-guard 0x2674940"},
    // GATE2@0x28ACF04: tbz originale (non raggiungibile con GATE1=NOP) -> nessun probe CS qui.
    {0x28acf08,0x2c3a6f8,CS_BL,31, "eb.ProcessPurchase","buf-alloc 0x2C3A6F8"},
    {0x28acf34,0x5463240,CS_BL,31, "eb.ProcessPurchase","str? 0x5463240"},
    {0x28acf38,0x28acffc,CS_B,31,  "eb.ProcessPurchase","PENDING -> b ret0"},
    {0x28acf40,0x2c3a7a0,CS_BL,31, "eb.ProcessPurchase","buf-alloc 0x2C3A7A0"},
    {0x28acf48,0x28ad00c,CS_CBZ,8, "eb.ProcessPurchase","GATE x8==null -> EXIT (niente grant)"},
    {0x28acf50,0x28ad00c,CS_CBZ,8, "eb.ProcessPurchase","GATE x8==null -> EXIT (niente grant)"},
    {0x28acf5c,0x48d5a84,CS_BL,31, "eb.ProcessPurchase","validate 0x48D5A84"},
    {0x28acf7c,0x2674940,CS_BL,31, "eb.ProcessPurchase","log-guard 0x2674940"},
    {0x28acf88,0x5462c30,CS_BL,31, "eb.ProcessPurchase","str? 0x5462C30"},
    {0x28acf90,0x28ad00c,CS_CBZ,8, "eb.ProcessPurchase","GATE x8==null -> EXIT (niente grant)"},
    {0x28acf98,0x28ad00c,CS_CBZ,9, "eb.ProcessPurchase","GATE x9==null -> EXIT (niente grant)"},
    {0x28acfa0,0x28ad00c,CS_CBZ,0, "eb.ProcessPurchase","GATE x0==null -> EXIT (niente grant)"},
    {0x28acfb4,0x2991924,CS_BL,31, "eb.ProcessPurchase","eao 0x2991924 (GRANT ENQUEUE)"},
    {0x28acfc8,0x2674a54,CS_BL,31, "eb.ProcessPurchase","alloc-obj 0x2674A54"},
    {0x28acfe4,0x49d5a44,CS_BL,31, "eb.ProcessPurchase","rk 0x49D5A44"},
    {0x28acff4,0x28acac4,CS_BL,31, "eb.ProcessPurchase","dne 0x28ACAC4"},
    {0x28ad00c,0x2674a64,CS_BL,31, "eb.ProcessPurchase","EXIT-path -> return-helper"},
    {0x28ad014,0x4a9bf94,CS_B,31,  "eb.ProcessPurchase","EXIT -> b shared-return"},
    // ===================== UserController.eao (0x2991924) =====================
    {0x2991958,0x2674828,CS_BL,31, "UserController.eao","init-helper 0x2674828"},
    {0x2991964,0x2674828,CS_BL,31, "UserController.eao","init-helper 0x2674828"},
    {0x2991988,0x2674a54,CS_BL,31, "UserController.eao","alloc queue-item 0x2674A54"},
    {0x2991994,0x28ae078,CS_BL,31, "UserController.eao","return-nil 0x28AE078"},
    {0x2991974,0x2991a20,CS_CBZ,8, "UserController.eao","GATE _user==null -> FAIL(0x2991a20)"},
    {0x2991998,0x2991a20,CS_CBZ,22,"UserController.eao","GATE item==null -> FAIL"},
    {0x29919a4,0x2991a20,CS_CBZ,20,"UserController.eao","GATE queue==null -> FAIL"},
    {0x29919c4,0x2991a20,CS_CBZ,8, "UserController.eao","GATE items==null -> FAIL"},
    {0x2991a00,0x367f6ac,CS_BL,31, "UserController.eao","list-grow 0x367F6AC"},
    {0x2991a1c,0x29905d8,CS_B,31,  "UserController.eao","tail-call -> flush 0x29905D8"},
    {0x2991a20,0x2674a64,CS_BL,31, "UserController.eao","FAIL-path return-helper"},
    // ===================== eao.flush (0x29905D8) =====================
    {0x2990608,0x2674828,CS_BL,31, "eao.flush","init-helper 0x2674828"},
    {0x2990614,0x2674828,CS_BL,31, "eao.flush","init-helper 0x2674828"},
    {0x2990620,0x2674828,CS_BL,31, "eao.flush","init-helper 0x2674828"},
    {0x299063c,0x54edaf0,CS_BL,31, "eao.flush","rk 0x54EDAF0"},
    {0x2990654,0x2674940,CS_BL,31, "eao.flush","log-guard 0x2674940"},
    {0x299065c,0x2c421b0,CS_BL,31, "eao.flush","rk 0x2C421B0"},
    {0x2990674,0x2674940,CS_BL,31, "eao.flush","log-guard 0x2674940"},
    {0x2990680,0x27c1330,CS_BL,31, "eao.flush","rk 0x27C1330"},
    {0x2990690,0x548ed08,CS_BL,31, "eao.flush","rk 0x548ED08"},
    {0x2990698,0x2c42450,CS_BL,31, "eao.flush","rk 0x2C42450"},
    {0x29906a0,0x548ee48,CS_BL,31, "eao.flush","rk 0x548EE48"},
    {0x29906b4,0x2674940,CS_BL,31, "eao.flush","log-guard 0x2674940"},
    {0x29906bc,0x2c42450,CS_BL,31, "eao.flush","rk 0x2C42450"},
    {0x29906c4,0x548ee84,CS_BL,31, "eao.flush","rk 0x548EE84"},
    {0x29906d0,0x2674940,CS_BL,31, "eao.flush","log-guard 0x2674940"},
    {0x29906d4,0x2674940,CS_BL,31, "eao.flush","log-guard 0x2674940"},
    {0x29906dc,0x2c424f8,CS_BL,31, "eao.flush","rk 0x2C424F8"},
    {0x29906e4,0x548ee48,CS_BL,31, "eao.flush","rk 0x548EE48"},
    {0x29906f0,0x2674940,CS_BL,31, "eao.flush","log-guard 0x2674940"},
    {0x2990700,0x2c424f8,CS_BL,31, "eao.flush","rk 0x2C424F8"},
    {0x2990708,0x548ee84,CS_BL,31, "eao.flush","rk 0x548EE84"},
    {0x299072c,0x2674940,CS_BL,31, "eao.flush","log-guard 0x2674940"},
    {0x2990758,0,CS_BR,2,   "eao.flush","TAIL br x2 (delegate finale; x30 preservato)"},
    // ===================== dne (0x28ACAC4) =====================
    {0x28acb08,0x2674828,CS_BL,31, "dne","init-helper 0x2674828"},
    {0x28acb14,0x2674828,CS_BL,31, "dne","init-helper 0x2674828"},
    {0x28acb20,0x2674828,CS_BL,31, "dne","init-helper 0x2674828"},
    {0x28acb2c,0x2674828,CS_BL,31, "dne","init-helper 0x2674828"},
    {0x28acb3c,0x2674a54,CS_BL,31, "dne","alloc-obj 0x2674A54"},
    {0x28acb48,0x4a9bf94,CS_BL,31, "dne","shared-return 0x4A9BF94"},
    {0x28acb4c,0x28acc60,CS_CBZ,19,"dne","GATE x19==null -> FAIL"},
    {0x28acb58,0x28acc60,CS_CBZ,21,"dne","GATE x21==null -> FAIL"},
    {0x28acb60,0x28acc60,CS_CBZ,8, "dne","GATE x8==null -> FAIL"},
    {0x28acb80,0x2674940,CS_BL,31, "dne","log-guard 0x2674940"},
    {0x28acb90,0x4ab7ee4,CS_BL,31, "dne","rk 0x4AB7EE4"},
    {0x28acb98,0x28acc60,CS_CBZ,9, "dne","GATE x9==null -> FAIL"},
    {0x28acba0,0x28acc60,CS_CBZ,8, "dne","GATE x8==null -> FAIL"},
    {0x28acba8,0x28acc60,CS_CBZ,9, "dne","GATE x9==null -> FAIL"},
    {0x28acbc4,0x4ab684c,CS_BL,31, "dne","rk 0x4AB684C"},
    {0x28acbcc,0x28acc60,CS_CBZ,8, "dne","GATE x8==null -> FAIL"},
    {0x28acbd4,0x28acc60,CS_CBZ,9, "dne","GATE x9==null -> FAIL"},
    {0x28acbfc,0x2674a54,CS_BL,31, "dne","alloc-obj 0x2674A54"},
    {0x28acc10,0x412573c,CS_BL,31, "dne","rk 0x412573C"},
    {0x28acc14,0x28acc60,CS_CBZ,20,"dne","GATE x20==null -> FAIL"},
    {0x28acc3c,0x2a81454,CS_BL,31, "dne","rk 0x2A81454"},
    {0x28acc60,0x2674a64,CS_BL,31, "dne","FAIL-path return-helper"},
};

static uint32_t g_cs_seq = 0;
// Chiamata dalla stub con x0..x5 = argomenti reali del call-site (arrivano intatti:
// la stub full-preserve li salva/ripristina, tocca solo x16 prima del blr), x6 =
// &CS_TABLE[i], x7 = valore del registro indiretto/testato (0 per i call diretti).
static void store_cs_logger(void *x0, void *x1, void *x2, void *x3, void *x4, void *x5, void *x6, void *x7) {
    const struct CsSite *d = (const struct CsSite *)x6;
    unsigned seq = __atomic_add_fetch(&g_cs_seq, 1u, __ATOMIC_RELAXED);
    if (!d) { LOGI("STORE_TRACE[%u]: NULL desc x6=%p", seq, x6); return; }
    const char *k = d->kind == CS_BLR ? "blr" : d->kind == CS_BR ? "br" : d->kind == CS_B ? "b" : d->kind == CS_CBZ ? "cbz" : "bl";
    if (d->kind == CS_BLR || d->kind == CS_BR)
        LOGI("STORE_TRACE[%u] %s+0x%X -> [%s] %s reg=x%d val=%p | args x0=%p x1=%p x2=%p x3=%p x4=%p x5=%p",
             seq, d->caller, d->site, k, d->callee, d->reg, x7, x0, x1, x2, x3, x4, x5);
    else if (d->kind == CS_CBZ)
        LOGI("STORE_TRACE[%u] %s+0x%X -> [%s] %s reg=x%d val=%p -> %s(0x%X)",
             seq, d->caller, d->site, k, d->callee, d->reg, (void *)x7, "EXIT", d->tgt);
    else
        LOGI("STORE_TRACE[%u] %s+0x%X -> [%s] %s | args x0=%p x1=%p x2=%p x3=%p x4=%p x5=%p",
             seq, d->caller, d->site, k, d->callee, x0, x1, x2, x3, x4, x5);
}

// Stub call-site full-preserve (0x180): stessi prologhi/epiloghi gia' verificati di dxn.
// Layout: idx0..32 save | idx33 ldr x6,desc@0x140 | idx34 mov x7,xN/xzr |
//         idx35 ldr x16,log@0x128 | idx36 blr | idx37..69 restore | idx70..73 tail | literals.
// POOL: le stub vengono ritagliate da un unico pool contiguo allocato vicino al code base.
// Se uno stub finisse fuori +/-128MB (imm26) la patch viene SALTATA: un b troncato = jmp
// garbage = crash (causa dei SIGSEGV 4.71). Il tail CBZ usa SOLO literal assoluti (nessun
// limite imm19/imm26), quindi funziona anche se il target EXIT e' lontano.
static uint8_t *g_cs_pool = nullptr;
static size_t   g_cs_pool_size = 0;
static size_t   g_cs_pool_off = 0;

// Verifica che l'istruzione ORIGINALE al call-site corrisponda al tipo dichiarato in CS_TABLE.
// Se non combacia, la patch viene SALTATA: un tipo sbagliato (es. br tail trattato come blr)
// corrompe x30 -> il ret salta su garbage -> SIGSEGV eseguendo lo stack. Meglio perdere un log
// che far crashare l'app. Encodings ARM64 verificati con clang/objdump (regola 9k).
static bool cs_kind_ok(unsigned kind, uint32_t insn) {
    switch (kind) {
        case CS_BL:  return (insn & 0xFC000000u) == 0x94000000u;  // bl imm26
        case CS_B:   return (insn & 0xFC000000u) == 0x14000000u;  // b imm26
        case CS_BLR: return (insn & 0xFFFFFC1Fu) == 0xD63F0000u;  // blr Xn
        case CS_BR:  return (insn & 0xFFFFFC1Fu) == 0xD61F0000u;  // br Xn
        case CS_CBZ: return (insn & 0x7F000000u) == 0xB4000000u;  // cbz Xn
        default:     return false;
    }
}

static void install_call_site_probe(const struct CsSite *d) {
    if (!g_code_base) { LOGE("callsite[s=%X]: no code_base", d->site); return; }
    uintptr_t site = g_code_base + d->site;
    if (!g_cs_pool) { LOGE("callsite[s=%X]: no stub pool", d->site); return; }
    uint32_t orig_insn = 0;
    if (!safe_read_self(site, &orig_insn, 4)) { LOGE("callsite[s=%X]: read FAILED", d->site); return; }
    if (!cs_kind_ok(d->kind, orig_insn)) {
        LOGE("callsite[s=%X]: OPCODE MISMATCH kind=%u insn=%08X -> SKIP (nessuna patch)",
             d->site, d->kind, orig_insn);
        return;
    }
    if (g_cs_pool_off + 0x180 > g_cs_pool_size) { LOGE("callsite[s=%X]: pool exhausted", d->site); return; }
    uintptr_t stub_addr = (uintptr_t)(g_cs_pool + g_cs_pool_off);
    g_cs_pool_off += 0x180;
    uint32_t *s = (uint32_t *)stub_addr;

    // Range-check del branch site->stub PRIMA di scrivere (imm26 = +/-128MB, allineato 4).
    int64_t b_delta = (int64_t)stub_addr - (int64_t)site;
    if (b_delta < -(1LL << 27) || b_delta >= (1LL << 27) || (b_delta & 3)) {
        LOGE("callsite[s=%X]: stub %p out of imm26 range (delta=%lld) -> SKIP (nessuna patch)",
             d->site, (void *)stub_addr, (long long)b_delta);
        return;
    }

    static const uint32_t SAVE[33] = {
        0xd11403ff,0xa90007e0,0xa9010fe2,0xa90217e4,0xa9031fe6,0xa90427e8,0xa9052fea,0xa90637ec,0xa9073fee,0xa90847f0,
        0xa9094ff2,0xa90a57f4,0xa90b5ff6,0xa90c67f8,0xa90d6ffa,0xa90e77fc,0xf9007bfe,0xad0807e0,0xad090fe2,0xad0a17e4,
        0xad0b1fe6,0xad0c27e8,0xad0d2fea,0xad0e37ec,0xad0f3fee,0xad1047f0,0xad114ff2,0xad1257f4,0xad135ff6,0xad1467f8,
        0xad156ffa,0xad1677fc,0xad177ffe
    };
    static const uint32_t RESTORE[33] = {
        0xad577ffe,0xad5677fc,0xad556ffa,0xad5467f8,0xad535ff6,0xad5257f4,0xad514ff2,0xad5047f0,0xad4f3fee,0xad4e37ec,
        0xad4d2fea,0xad4c27e8,0xad4b1fe6,0xad4a17e4,0xad490fe2,0xad4807e0,0xf9407bfe,0xa94e77fc,0xa94d6ffa,0xa94c67f8,
        0xa94b5ff6,0xa94a57f4,0xa9494ff2,0xa94847f0,0xa9473fee,0xa94637ec,0xa9452fea,0xa94427e8,0xa9431fe6,0xa94217e4,
        0xa9410fe2,0xa94007e0,0x911403ff
    };
    for (int i = 0; i < 33; i++) { s[i] = SAVE[i]; }
    s[33] = 0x580005E6;                     // ldr x6, desc@0x140 (imm19=47)
    s[34] = d->reg != 31 ? (0xAA0003E7 | (d->reg << 16)) : 0xAA1F03E7; // mov x7, xN / xzr
    s[35] = 0x580004F0;                     // ldr x16, log@0x128 (imm19=39)
    s[36] = 0xD63F0200;                     // blr x16
    for (int i = 0; i < 33; i++) { s[37 + i] = RESTORE[i]; }

    uint32_t *t0 = s + 70;  // 0x118
    uint32_t *t1 = s + 71;  // 0x11C
    uint32_t *t2 = s + 72;  // 0x120
    uint32_t *t3 = s + 73;  // 0x124
    if (d->kind == CS_BL) {
        t0[0] = 0x580000DE; t1[0] = 0x580000F0; t2[0] = 0xD61F0200;   // ldr x30,ret@0x130 ; ldr x16,tgt@0x138 ; br x16
    } else if (d->kind == CS_B) {
        t0[0] = 0xD503201F; t1[0] = 0x580000F0; t2[0] = 0xD61F0200;   // nop ; ldr x16,tgt@0x138 ; br x16 (x30 NON toccato)
    } else if (d->kind == CS_BLR) {
        t0[0] = 0x580000DE; t1[0] = 0xD61F0000 | (d->reg << 5); t2[0] = 0xD503201F; // ldr x30,ret ; br xN ; nop
    } else if (d->kind == CS_BR) {
        t0[0] = 0xD503201F; t1[0] = 0xD61F0000 | (d->reg << 5); t2[0] = 0xD503201F; // nop ; br xN ; nop (x30 NON toccato: tail-call)
    } else {  // CS_CBZ: target via literal assoluti (encodings verificati clang/objdump 9k)
        t0[0] = 0x58000190;               // ldr x16, exit@0x148 (pc+0x30)
        t1[0] = 0xB4000040 | d->reg;      // cbz xN, +8 (salta il ldr successivo se null)
        t2[0] = 0x58000090;               // ldr x16, next@0x130 (pc+0x10)
        t3[0] = 0xD61F0200;               // br x16
    }

    *(uintptr_t *)(stub_addr + 0x128) = (uintptr_t)(uintptr_t)store_cs_logger;
    *(uintptr_t *)(stub_addr + 0x130) = (uintptr_t)(site + 4);
    *(uintptr_t *)(stub_addr + 0x138) = (uintptr_t)(g_code_base + ((d->kind == CS_BLR || d->kind == CS_BR) ? 0 : d->tgt));
    *(uintptr_t *)(stub_addr + 0x140) = (uintptr_t)d;
    *(uintptr_t *)(stub_addr + 0x148) = (uintptr_t)(g_code_base + d->tgt);   // CS_CBZ: target EXIT
    __builtin___clear_cache((char *)stub_addr, (char *)stub_addr + 0x160);

    uint32_t b_ins = 0x14000000 | (((uint32_t)(b_delta / 4)) & 0x3FFFFFF);
    if (!proc_mem_write(site, &b_ins, 4)) { LOGE("callsite[s=%X] patch write FAILED", d->site); return; }
    __builtin___clear_cache((char *)site, (char *)site + 4);
    LOGI("callsite probe: %s+0x%X -> %s kind=%u stub=%p (d=%lld)", d->caller, d->site, d->callee, d->kind, (void *)stub_addr, (long long)b_delta);
}

// ==================== v55: smart-grant CHECK bypass (inside 0x28a94d4) ====================
// Dopo fix #1 + #2 il grant path in ProcessPurchase arriva a:
//   0x28a94d4: ... bl 0x28b1984 (CHECK: 4 validatori in catena) ...
//   0x28a957c: tbz w0, #0x0, 0x28a95bc   // se check fallisce → exit senza grant
//   0x28a9580: ...proceed to 0x28bc3e8 (REAL GRANT / reward erogation)...
// Il CHECK fallisce sempre per un purchase fake (firma non valida, receipt non verificabile).
// FIX: NOP sul tbz → se check fallisce, il salto NON viene eseguito e si prosegue
// al vrai grant a 0x28bc3e8. Il NOP e' il patch piu' sicuro possibile (nessun salto,
// nessun registro toccato, 1 sola istruzione).
// NOTA v56: non applicabile (v56 non ha chain validator; il metodo finisce a ~entry+0x2D8).
// La funzione viene lasciata DEFINITA ma NON chiamata dal worker.
static void install_smart_grant_check_nop(void) {
    if (!g_proc_purchase_entry) { LOGE("smart check nop: g_proc_purchase_entry not set"); return; }
    // RVA 0x28a957c = ProcessPurchase entry (0x28a9188) + 0x3F4
    uintptr_t target = g_proc_purchase_entry + 0x3F4;
    // Verify original bytes: tbz w0,#0 = 0x36000200 (little-endian bytes: 00 02 00 36)
    uint32_t orig = 0;
    if (safe_read_self(target, &orig, 4)) {
        LOGI("smart check nop: original @entry+0x3F4 = %08x", orig);
        if (orig != 0x36000200) {
            LOGE("smart check nop: UNEXPECTED bytes %08x (expected 0x36000200 tbz w0,#0) — aborting", orig);
            return;
        }
    } else {
        LOGE("smart check nop: safe_read FAILED"); return;
    }
    uint32_t nop = 0xD503201F;  // NOP
    if (proc_mem_write(target, &nop, 4)) {
        __builtin___clear_cache((char*)target, (char*)target + 4);
        uint32_t verify = 0;
        safe_read_self(target, &verify, 4);
        LOGI("smart check nop: patched tbz@entry+0x3F4 -> NOP (bypass 4-validator chain, force grant)");
        LOGI("smart check nop: verify readback = %08x (expect D503201F)", verify);
    } else {
        LOGE("smart check nop: proc_mem_write FAILED");
    }
}

// ==================== Reward fn 0x28c2238 (=dxn) entry trace ====================
// L'erog gate (install_erog_gate) salta SEMPRE a 0x28af0f8 -> tail call 0x28c2238 (=dxn).
// dxn e' un metodo instance managed con firma (double, string, string, string, string,
// Action<bool,string,bool>): gli argomenti entry sono x0(self), x1..x5, v0(d0=double).
// PERCHE' LO STUDIAMO: il log 4.43 mostra VERIFICA VALIDATOR=FALSE + flusso forzato verso
// erogazione, ma nessun reward erogato e my_dnf NON scatta. Vogliamo confermare che il
// tail-call verso dxn (0x28c2238) venga davvero eseguito.
// HOOK SICURO: stub assembly che salva/ripristina TUTTI i registri (x0..x30, q0..q31),
// logga l'ingresso, poi br al trampoline originale. Cosi' dxn gira normalmente.
static void dxn_entry_logger(void) {
    LOGI("REWARD_ENTRY: 0x28c2238 (dxn) REACHED - erogation tail-call executes");
}

static void install_dxn_entry_trace(void) {
    uintptr_t off = 0x28BE238;   // dump.cs Offset di dxn (RVA 0x28C2238)
    uintptr_t target = g_il2cpp_base + off;
    void *stub = alloc_exec_near((void*)target, 0x140);
    if (!stub) { LOGE("dxn trace: alloc_exec_near failed"); return; }
    uint32_t *s = (uint32_t*)stub;

    // LSU/ALU prologue+epilogue, encodings VERIFICATI con clang (9k). Layout:
    // code 0x00..0x114 (70 istruzioni), log_fn literal @0x118, orig_fn literal @0x120.
    static const uint32_t CODE[70] = {
        0xd11403ff,0xa90007e0,0xa9010fe2,0xa90217e4,0xa9031fe6,0xa90427e8,0xa9052fea,0xa90637ec,0xa9073fee,0xa90847f0,
        0xa9094ff2,0xa90a57f4,0xa90b5ff6,0xa90c67f8,0xa90d6ffa,0xa90e77fc,0xf9007bfe,0xad0807e0,0xad090fe2,0xad0a17e4,
        0xad0b1fe6,0xad0c27e8,0xad0d2fea,0xad0e37ec,0xad0f3fee,0xad1047f0,0xad114ff2,0xad1257f4,0xad135ff6,0xad1467f8,
        0xad156ffa,0xad1677fc,0xad177ffe,0x580004b0,0xd63f0200,0xad577ffe,0xad5677fc,0xad556ffa,0xad5467f8,0xad535ff6,
        0xad5257f4,0xad514ff2,0xad5047f0,0xad4f3fee,0xad4e37ec,0xad4d2fea,0xad4c27e8,0xad4b1fe6,0xad4a17e4,0xad490fe2,
        0xad4807e0,0xf9407bfe,0xa94e77fc,0xa94d6ffa,0xa94c67f8,0xa94b5ff6,0xa94a57f4,0xa9494ff2,0xa94847f0,0xa9473fee,
        0xa94637ec,0xa9452fea,0xa94427e8,0xa9431fe6,0xa94217e4,0xa9410fe2,0xa94007e0,0x911403ff,0x58000090,0xd61f0200
    };
    // ldr x16,log_fn@idx33: imm19 = (0x118-0x84)/4 = 37 (encoding presente in CODE[33]=0x580004b0).
    // ldr x16,orig_fn@idx68: imm19 = (0x120-0x110)/4 = 4  (encoding in CODE[68]=0x58000090).
    for (int i = 0; i < 70; i++) s[i] = CODE[i];
    *(uintptr_t*)((uint8_t*)stub + 0x118) = (uintptr_t)(uintptr_t)dxn_entry_logger;
    __builtin___clear_cache((char*)stub, (char*)stub + 0x128);
    LOGI("dxn trace: installing hook @%p (offset 0x%X) stub=%p", (void*)target, (unsigned int)off, stub);
    void *backup = inline_hooker((void*)target, stub);
    if (!backup) { LOGE("dxn trace: inline_hooker failed"); return; }
    *(uintptr_t*)((uint8_t*)stub + 0x120) = (uintptr_t)backup;
    __builtin___clear_cache((char*)stub, (char*)stub + 0x128);
    LOGI("dxn trace: OK backup=%p (log-only, full reg preserve)", backup);
}

// ==================== GRANT CHAIN PROBES (v4.56, log-only) ====================
// Scope: capire DOVE si interrompe la catena di erogazione per lo store. Verificato
// con disassembly di libil2cpp (split-55):
//   ProcessPurchase(0x28a9188) -> fallback@entry+0x160 (0x28a92e8) -> GRANTBL
//     -> bl 0x298f448 @0x28a93d0  (primo grant: bl da fallback)
//     -> bl 0x28a8f1c  @0x28a9410  (dw.dlz dispatcher)
//        -> bl 0x28a94d4 @0x28a9090 (smart-grant orchestrator)
//           -> tail-call b 0x28bc3e8 @0x28a95b8 (REAL GRANT, 6-arg + v0)
// Tutti i 4 ingressi sono prologhi "__lto/apin", solo stack ops nei primi 16 byte
// -> SICURI per inline_hooker (16-byte). Stessa stub full-preserve verificata di dxn.
static void grant_probe_298f448_logger(void) {
    LOGI("GRANT_CHAIN_298f448: ENTRY (bl da fallback GRANTBL) — primo grant eseguito");
}
static void grant_probe_28a8f1c_logger(void) {
    LOGI("GRANT_CHAIN_28a8f1c: ENTRY (dw.dlz dispatcher bl @0x28a9410)");
}
static void grant_probe_28a94d4_logger(void) {
    LOGI("GRANT_CHAIN_28a94d4: ENTRY (smart-grant orchestrator bl @0x28a9090)");
}
// v4.59: tracing descriptor REAL GRANT. La stub full-preserve chiama il logger via
// blr x16 mentre x0..x5 contengono ANCORA i registri originali di entry (non clobbrati
// dalle stp/adp di salvataggio, unico registro toccato e' x16). Quindi un logger con
// firma (x0..x5) riceve esattamente gli argomenti di 0x28bc3e8:
//   x0=product_ctx, x1, x2, x3, x4(=arg4 id offerta), x5
// Leggiamo anche i campi di product_ctx che alimentano il grant descriptor
// ([+0x50] qword prezzo/quantita, [+0x60], [+0x68]) per distinguere A/B/C.
static void grant_probe_28bc3e8_logger(void *x0, void *x1, void *x2, void *x3, void *x4, void *x5) {
    uint64_t q50=0, q58=0, q60=0, q68=0, q78=0;
    uintptr_t x0v = (uintptr_t)x0;
    if (x0v) {
        safe_read_self(x0v + 0x50, &q50, 8);
        safe_read_self(x0v + 0x58, &q58, 8);
        safe_read_self(x0v + 0x60, &q60, 8);
        safe_read_self(x0v + 0x68, &q68, 8);
        safe_read_self(x0v + 0x78, &q78, 8);
    }
    LOGI("GRANT_CHAIN_28bc3e8: ENTRY (REAL GRANT b @0x28a95b8) args x0=%p x1=%p x2=%p x3=%p x4=%p x5=%p",
         x0, x1, x2, x3, x4, x5);
    LOGI("  ctx[+0x50]=0x%llx [+0x58]=0x%llx [+0x60]=0x%llx [+0x68]=0x%llx [+0x78]=0x%llx",
         (unsigned long long)q50, (unsigned long long)q58, (unsigned long long)q60,
         (unsigned long long)q68, (unsigned long long)q78);
}

static void install_grant_entry_probe(uintptr_t rva, uintptr_t logger, const char *which) {
    if (!g_code_base) { LOGE("grant probe[%s]: no code_base (store hook failed)", which); return; }
    uintptr_t off = rva;
    uintptr_t target = g_code_base + off;
    void *stub = alloc_exec_near((void*)target, 0x140);
    if (!stub) { LOGE("grant probe[%s]: alloc_exec_near failed", which); return; }
    uint32_t *s = (uint32_t*)stub;

    // Stessa stub full-preserve verificata con clang (9k) di install_dxn_entry_trace.
    // Idx33 ldr x16,log_fn@0x118 | idx68 ldr x16,orig_fn@0x120.
    static const uint32_t CODE[70] = {
        0xd11403ff,0xa90007e0,0xa9010fe2,0xa90217e4,0xa9031fe6,0xa90427e8,0xa9052fea,0xa90637ec,0xa9073fee,0xa90847f0,
        0xa9094ff2,0xa90a57f4,0xa90b5ff6,0xa90c67f8,0xa90d6ffa,0xa90e77fc,0xf9007bfe,0xad0807e0,0xad090fe2,0xad0a17e4,
        0xad0b1fe6,0xad0c27e8,0xad0d2fea,0xad0e37ec,0xad0f3fee,0xad1047f0,0xad114ff2,0xad1257f4,0xad135ff6,0xad1467f8,
        0xad156ffa,0xad1677fc,0xad177ffe,0x580004b0,0xd63f0200,0xad577ffe,0xad5677fc,0xad556ffa,0xad5467f8,0xad535ff6,
        0xad5257f4,0xad514ff2,0xad5047f0,0xad4f3fee,0xad4e37ec,0xad4d2fea,0xad4c27e8,0xad4b1fe6,0xad4a17e4,0xad490fe2,
        0xad4807e0,0xf9407bfe,0xa94e77fc,0xa94d6ffa,0xa94c67f8,0xa94b5ff6,0xa94a57f4,0xa9494ff2,0xa94847f0,0xa9473fee,
        0xa94637ec,0xa9452fea,0xa94427e8,0xa9431fe6,0xa94217e4,0xa9410fe2,0xa94007e0,0x911403ff,0x58000090,0xd61f0200
    };
    for (int i = 0; i < 70; i++) s[i] = CODE[i];
    *(uintptr_t*)((uint8_t*)stub + 0x118) = (uintptr_t)(uintptr_t)logger;
    __builtin___clear_cache((char*)stub, (char*)stub + 0x128);
    LOGI("grant probe[%s]: installing hook @%p (offset 0x%X) stub=%p", which, (void*)target, (unsigned int)off, stub);
    void *backup = inline_hooker((void*)target, stub);
    if (!backup) { LOGE("grant probe[%s]: inline_hooker failed", which); return; }
    *(uintptr_t*)((uint8_t*)stub + 0x120) = (uintptr_t)backup;
    __builtin___clear_cache((char*)stub, (char*)stub + 0x128);
    LOGI("grant probe[%s]: OK backup=%p (log-only, full reg preserve)", which, backup);
}

static void install_grant_chain_probes(void) {
    if (!g_code_base) { LOGE("grant chain probes: no code_base (store hook failed)"); return; }
    install_grant_entry_probe(0x298F448, (uintptr_t)(uintptr_t)grant_probe_298f448_logger, "298f448");
    install_grant_entry_probe(0x28A8F1C, (uintptr_t)(uintptr_t)grant_probe_28a8f1c_logger, "28a8f1c");
    install_grant_entry_probe(0x28A94D4, (uintptr_t)(uintptr_t)grant_probe_28a94d4_logger, "28a94d4");
    install_grant_entry_probe(0x28BC3E8, (uintptr_t)(uintptr_t)grant_probe_28bc3e8_logger, "28bc3e8");
}

// ==================== v4.71: STORE CHAIN ENTRY HOOKS (eao/dne/flush) ====================
static void st_chain_entry_eao(void *x0, void *x1, void *x2, void *x3, void *x4, void *x5) {
    uintptr_t user = 0, queue = 0; uint32_t qc = 0;
    if (x0) { safe_read_self((uintptr_t)x0 + 0x28, &user, 8); }
    if (user) { safe_read_self(user + 0xa8, &queue, 8); }
    if (queue) { safe_read_self(queue + 0x18, &qc, 4); }
    LOGI("STORE_CHAIN_ENTRY: eao(0x2991924) x0=%p x1=%p x2=%p x3=%p x4=%p x5=%p user(+0x28)=%p queue(+0xA8)=%p count=%u",
         x0, x1, x2, x3, x4, x5, (void *)user, (void *)queue, (unsigned)qc);
}
static void st_chain_entry_dne(void *x0, void *x1, void *x2, void *x3, void *x4, void *x5) {
    LOGI("STORE_CHAIN_ENTRY: dne(0x28ACAC4) x0=%p x1=%p x2=%p x3=%p x4=%p x5=%p", x0, x1, x2, x3, x4, x5);
}
static void st_chain_entry_flush(void *x0, void *x1, void *x2, void *x3, void *x4, void *x5) {
    uintptr_t u = 0, q = 0; uint32_t qc = 0;
    if (x0) { safe_read_self((uintptr_t)x0 + 0x28, &u, 8); }
    if (u) { safe_read_self(u + 0xa8, &q, 8); }
    if (q) { safe_read_self(q + 0x18, &qc, 4); }
    // v4.81: apprende g_user_base qui (user data object, contiene le dungeon keys).
    if (u && !g_user_base) {
        g_user_base = (uintptr_t)u;
        LOGI("USER_BASE: appreso da flush hook user(+0x28) = 0x%zX", (size_t)g_user_base);
    }
    LOGI("STORE_CHAIN_ENTRY: flush(0x29905D8) x0=%p x1=%p user(+0x28)=%p queue(+0xA8)=%p count=%u",
         x0, x1, (void *)u, (void *)q, (unsigned)qc);
}

static void install_store_chain_trace(void) {
    if (!g_code_base) { LOGE("STORE_TRACE: no code_base (store hook failed)"); return; }
    size_t n = sizeof(CS_TABLE) / sizeof(CS_TABLE[0]);
    g_cs_pool_size = ((n * 0x180) + 0xFFF) & ~(size_t)0xFFF;
    g_cs_pool_off = 0;
    void *anchor = (void *)(g_code_base + 0x2900000);   // centro dello span dei site
    g_cs_pool = (uint8_t *)alloc_exec_near(anchor, g_cs_pool_size);
    if (!g_cs_pool) { LOGE("STORE_TRACE: stub pool alloc failed -> abort (nessuna patch)"); return; }
    LOGI("STORE_TRACE: stub pool @%p size=%u (anchor=%p delta=%lld)",
         g_cs_pool, (unsigned)g_cs_pool_size, anchor,
         (long long)((int64_t)(uintptr_t)g_cs_pool - (int64_t)(uintptr_t)anchor));
    LOGI("STORE_TRACE: installing %u call-site probes (catena store v56: log OGNI movimento)", (unsigned)n);
    for (size_t i = 0; i < n; i++) { install_call_site_probe(&CS_TABLE[i]); }
    LOGI("STORE_TRACE: done - %u call-site probes", (unsigned)n);
    // Entry hook 16-byte sulle funzioni VIVE per ULTIME (le patch 4-byte dei call-site sono atomiche)
    install_grant_entry_probe(0x2991924, (uintptr_t)(uintptr_t)st_chain_entry_eao, "eao-entry");
    LOGI("STORE_TRACE: eao(0x2991924) entry hook installed");
    install_grant_entry_probe(0x28ACAC4, (uintptr_t)(uintptr_t)st_chain_entry_dne, "dne-entry");
    LOGI("STORE_TRACE: dne(0x28ACAC4) entry hook installed");
    install_grant_entry_probe(0x29905D8, (uintptr_t)(uintptr_t)st_chain_entry_flush, "flush-entry");
    LOGI("STORE_TRACE: flush(0x29905D8) entry hook installed");
    long ps = sysconf(_SC_PAGESIZE);
    uintptr_t pg = (uintptr_t)g_cs_pool & ~(uintptr_t)(ps - 1);
    size_t span = g_cs_pool_size + ((uintptr_t)g_cs_pool - pg);
    if (mprotect((void *)pg, span, PROT_READ | PROT_EXEC) == 0)
        LOGI("STORE_TRACE: pool mprotect RX ok (%zu bytes)", span);
    else
        LOGE("STORE_TRACE: pool mprotect RX failed errno=%d", errno);
}

// ==================== ENGINE DISPATCH PROBE (0x2a7ed18) ====================
// 0x2a7ed18 e' la funzione di dispatch CONDIVISA tra il percorso AD e il percorso
// purchase: chiamata da dxn (0x28c2140/0x28c21c0/0x28c229c) e da 0x27dabbc
// (orchestratore purchase). Firma d'uso: (x0=consumer/device, x1=payload,
// x2=0). Se l'acquisto raggiunge questo punto => il flusso arriva all'engine
// condiviso; se NON compare => il purchase non passa mai per l'engine.
//
// v4.62: passa dallo schema "stub full-preserve" (install_grant_entry_probe, blr
// clobbera x30) allo schema "br x16 wrapper" (install_post_trace_hook): il jmp
// su 16 byte non tocca x30, quindi il wrapper riceve il caller VERO della
// funzione via __builtin_return_address(0). 0x2a7ed18 ha un adrp nei primi 16
// byte (offset +0xC); fix_adrp_in_trampoline lo converte in ldr-literal (literal
// pool a tramp+0x20, verificato runtime con dli/dze da v4.61).
// THROTTLE: il flood normale (AD path) chiama 2a7ed18 in continuazione con pochi
// (caller,x1) fissi. La risposta purchase introduce caller/x1 NUOVI. Logghiamo:
//   - ogni caller mai visto prima (sempre)
//   - ogni cambio di x1 per lo stesso caller (nuovo payload)
//   - al massimo 1 riga/sec per coppia (caller,x1) stabile
//   - heartbeat ogni 4096 skip per confermare liveness
static void install_post_trace_hook(uintptr_t target, void *wrapper, void **orig_out, const char *which);
typedef void *(*dispatch_trace_fn)(void *x0, void *x1, void *x2, void *x3, unsigned int x4);
static void *g_orig_dispatch = nullptr;

static void *my_dispatch_trace(void *x0, void *x1, void *x2, void *x3, unsigned int x4) {
    static uintptr_t ring[8] = {0};
    static int ring_pos = 0;
    static uintptr_t last_x1 = 0;
    static uint64_t last_log_ns = 0;
    static uint64_t skip_count = 0;
    static uint64_t total_calls = 0;
    static uint64_t log_count = 0;

    total_calls++;
    uintptr_t caller = (uintptr_t)__builtin_return_address(0);

    // Check if caller is in the "known" ring
    bool known = false;
    for (int i = 0; i < 8; i++) {
        if (ring[i] == caller) { known = true; break; }
    }

    struct timespec ts;
    clock_gettime(CLOCK_MONOTONIC, &ts);
    uint64_t now_ns = (uint64_t)ts.tv_sec * 1000000000ULL + (uint64_t)ts.tv_nsec;

    bool caller_new = !known;
    bool x1_changed = (uintptr_t)x1 != last_x1;
    bool cooldown_expired = (now_ns - last_log_ns) >= 1000000000ULL; // 1s

    if (caller_new || x1_changed || cooldown_expired) {
        if (skip_count > 0 && !caller_new && !x1_changed) {
            // Cooldown expiry heartbeat — log skip count
            LOGI("DISPATCH_2a7ed18: HEARTBEAT (skipped %llu, logged %llu, total %llu)",
                 (unsigned long long)skip_count, (unsigned long long)log_count, (unsigned long long)total_calls);
        }
        LOGI("DISPATCH_2a7ed18: CALLED BY RVA=0x%llX (caller=%p)%s"
             " x0=%p x1=%p x2=%p x3=%p x4=0x%x [calls=%llu logged=%llu]",
             (unsigned long long)(caller - g_code_base), (void*)caller,
             caller_new ? " *NEW*" : "",
             x0, x1, x2, x3, (unsigned int)x4,
             (unsigned long long)total_calls, (unsigned long long)log_count);
        log_count++;
        skip_count = 0;
        last_log_ns = now_ns;
        last_x1 = (uintptr_t)x1;
        if (caller_new) {
            ring[ring_pos] = caller;
            ring_pos = (ring_pos + 1) % 8;
        }
    } else {
        skip_count++;
        if ((skip_count & 0xFFF) == 0) {
            LOGI("DISPATCH_2a7ed18: THROTTLED %llu skipped (last caller RVA=0x%llX, last x1=%p)",
                 (unsigned long long)skip_count, (unsigned long long)(ring[ring_pos ? ring_pos-1 : 7] - g_code_base),
                 (void*)last_x1);
        }
    }

    return g_orig_dispatch ? ((dispatch_trace_fn)g_orig_dispatch)(x0, x1, x2, x3, x4) : nullptr;
}

static void install_engine_dispatch_probe(void) {
    if (!g_code_base) { LOGE("engine dispatch probe: no code_base (store hook failed)"); return; }
    install_post_trace_hook(g_code_base + 0x2A7ED18, (void*)my_dispatch_trace, &g_orig_dispatch, "dispatch");
}

// ==================== POST TRACE PROBES (dli 0x27dabbc / dze 0x298f548) ====================
// v4.61: tracciamo l'orchestratore POST (dli) e il secondo punto di append pending
// (dze) per mappare CHI li chiama a runtime e trovare l'handler della risposta server.
// NOTA: dlh (0x27d5cdc) e' solo `and w4,w4,#1; b 0x27dabbc` (tail-call in dli):
// lo hooking su dli cattura TUTTE le POST (dirette o via dlh), quindi dlh NON viene
// hookato. Firme all'entry: dli usa (x0..x3, w4); dze usa (x0, x1) + scratch.
// Pattern install_store_hook/my_dnf: trampoline 16B + fix_adrp + wrapper C++ con
// __builtin_return_address(0) che restituisce il caller VERO della funzione.
typedef void *(*post_trace_fn)(void *x0, void *x1, void *x2, void *x3, unsigned int x4);
static void *g_orig_dli = nullptr;
static void *g_orig_dze = nullptr;

static void maybe_dump_string(void *p, const char *tag) {
    if (!p) { LOGI("POST_TRACE: %s = NULL", tag); return; }
    int32_t len = 0;
    if (!safe_read_self((uintptr_t)p + 0x10, &len, 4) || len <= 0 || len > 0x2000) {
        LOGI("POST_TRACE: %s = %p (non-string, len@+0x10=%d)", tag, p, len);
        return;
    }
    int n = len < 512 ? len : 512;
    char buf[512];
    // v4.79: chars il2cpp stanno a +0x14 (object 16B + int32 length), NON +0x18
    if (!safe_read_self((uintptr_t)p + 0x14, buf, n)) { LOGI("POST_TRACE: %s = %p (read fail)", tag, p); return; }
    char out[1024]; int oi = 0;
    for (int i = 0; i < n && oi < 1022; i++) {
        unsigned char c = (unsigned char)buf[i];
        out[oi++] = (c >= 0x20 && c < 0x7f) ? (char)c : '.';
    }
    out[oi] = 0;
    LOGI("POST_TRACE: %s = \"%s\" (len=%d)", tag, out, len);
}

// dli = orchestratore POST (tutte le richieste HTTP passano da qui)
// v4.64: quando chiamato dalla POST del grant reale (RA == base+0x28BC708 =
// bl 0x27d5cdc @0x28bc704 nel grant 0x28bc3e8), x3 e' il callback delegate creato
// da 0x411c4c4 con layout: [+0x10]=target handler (il vero response handler),
// [+0x18]=trampoline single invoke 0x2379940, [+0x38]=trampoline multicast 0x23798e8,
// [+0x20]=closure, [+0x28]=config. Catturiamo il target e lo logghiamo come RVA
// per il disasm statico del fix v4.65.
static void *g_cap_cb = nullptr;
static void *g_cap_hdl = nullptr;

static void dump_callback_fields(void *cb, const char *tag) {
    if (!cb) { LOGI("CB[%s]: x3=NULL", tag); return; }
    uint64_t t10=0,t18=0,t20=0,t28=0,t38=0;
    safe_read_self((uintptr_t)cb+0x10,&t10,8);
    safe_read_self((uintptr_t)cb+0x18,&t18,8);
    safe_read_self((uintptr_t)cb+0x20,&t20,8);
    safe_read_self((uintptr_t)cb+0x28,&t28,8);
    safe_read_self((uintptr_t)cb+0x38,&t38,8);
#define RVAIF(v) ((uint64_t)(v) >= (uint64_t)g_code_base ? (uint64_t)((uint64_t)(v)-(uint64_t)g_code_base) : (uint64_t)-1)
    LOGI("CB[%s]: cb=%p [+0x10]=%p(RVA 0x%llX) [+0x18]=%p(RVA 0x%llX) [+0x20]=%p(RVA 0x%llX) [+0x28]=%p(RVA 0x%llX) [+0x38]=%p(RVA 0x%llX)",
         tag, cb, (void*)t10, (unsigned long long)RVAIF(t10), (void*)t18, (unsigned long long)RVAIF(t18),
         (void*)t20, (unsigned long long)RVAIF(t20), (void*)t28, (unsigned long long)RVAIF(t28),
         (void*)t38, (unsigned long long)RVAIF(t38));
#undef RVAIF
}

static void *my_dli_trace(void *x0, void *x1, void *x2, void *x3, unsigned int x4) {
    uintptr_t ra = (uintptr_t)__builtin_return_address(0);
    LOGI("POST_TRACE dli(0x27dabbc): CALLED BY %p x0=%p x1=%p x2=%p x3=%p x4=0x%x",
         (void*)ra, x0, x1, x2, x3, x4);
    maybe_dump_string(x1, "dli.x1");
    maybe_dump_string(x2, "dli.x2");
    maybe_dump_string(x3, "dli.x3");
    if (g_code_base && (ra == (uintptr_t)(g_code_base + 0x28BC708) ||
                        ra == (uintptr_t)(g_code_base + 0x28F0CF0) ||
                        ra == (uintptr_t)(g_code_base + 0x26BAFAC))) {
        const char *tag = ra == (uintptr_t)(g_code_base + 0x28BC708) ? "grant"
                        : ra == (uintptr_t)(g_code_base + 0x28F0CF0)  ? "adOverdriv"
                                                                      : "claim";
        LOGI("CB: POST detected (RA=%p RVA 0x%llX tag=%s)", (void*)ra,
             (unsigned long long)(ra - (uint64_t)g_code_base), tag);
        dump_callback_fields(x3, tag);
        if (ra == (uintptr_t)(g_code_base + 0x28BC708) && x3) {
            uint64_t hdl = 0;
            safe_read_self((uintptr_t)x3 + 0x10, &hdl, 8);
            g_cap_cb = x3;
            g_cap_hdl = (void*)hdl;
            LOGI("CB: captured handler=%p (RVA 0x%llX)",
                 g_cap_hdl, (unsigned long long)((uint64_t)g_cap_hdl - (uint64_t)g_code_base));
        }
    }
    return g_orig_dli ? ((post_trace_fn)g_orig_dli)(x0, x1, x2, x3, x4) : nullptr;
}

// dze = secondo punto di append pending (letto in [x0+0x28] -> [+0xa8] pending list)
static void *my_dze_trace(void *x0, void *x1, void *x2, void *x3, unsigned int x4) {
    LOGI("POST_TRACE dze(0x298f548): CALLED BY %p x0=%p x1=%p x2=%p x3=%p x4=0x%x",
         __builtin_return_address(0), x0, x1, x2, x3, x4);
    uintptr_t u = (uintptr_t)x0;
    uint64_t inner = 0, list = 0; uint32_t count = 0;
    if (u) { safe_read_self(u + 0x28, &inner, 8); }
    if (inner) { safe_read_self(inner + 0xa8, &list, 8); }
    if (list) { safe_read_self(list + 0x18, &count, 4); }
    LOGI("POST_TRACE dze: [x0+0x28]=%p [inner+0xa8]=%p pendingList=%p count=0x%x",
         (void*)inner, (void*)list, (void*)list, count);
    maybe_dump_string(x1, "dze.x1");
    return g_orig_dze ? ((post_trace_fn)g_orig_dze)(x0, x1, x2, x3, x4) : nullptr;
}

static void install_post_trace_hook(uintptr_t target, void *wrapper, void **orig_out, const char *which) {
    uint8_t *b = (uint8_t*)target;
    LOGI("POST_TRACE install[%s]: target=%p bytes %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x",
         which, (void*)target,
         b[0],b[1],b[2],b[3],b[4],b[5],b[6],b[7],b[8],b[9],b[10],b[11],b[12],b[13],b[14],b[15]);
    void *tramp = alloc_exec_near((void*)target, 64);
    if (!tramp) { LOGE("POST_TRACE[%s]: alloc_exec_near failed", which); return; }
    memcpy(tramp, (void*)target, 16);
    fix_adrp_in_trampoline(tramp, (void*)target);
    uint32_t *t = (uint32_t*)tramp;
    t[4] = 0x58000050;   // ldr x16, #8
    t[5] = 0xD61F0200;   // br x16
    *(uint64_t*)&t[6] = (uint64_t)target + 16;
    __builtin___clear_cache((char*)tramp, (char*)tramp + 64);
    uint8_t jmp[16];
    *(uint32_t*)&jmp[0] = 0x58000050;
    *(uint32_t*)&jmp[4] = 0xD61F0200;
    *(uint64_t*)&jmp[8] = (uint64_t)wrapper;
    if (proc_mem_write((uintptr_t)target, jmp, 16)) {
        __builtin___clear_cache((char*)target, (char*)target + 16);
        if (memcmp((void*)target, jmp, 16) == 0) {
            *orig_out = tramp;
            LOGI("POST_TRACE[%s]: OK (trampoline=%p)", which, tramp);
        } else LOGE("POST_TRACE[%s]: verify FAILED", which);
    } else LOGE("POST_TRACE[%s]: proc_mem_write FAILED", which);
}

static void install_post_trace_probes(void) {
    if (!g_code_base) { LOGE("POST_TRACE: no g_code_base (store hook failed)"); return; }
    install_post_trace_hook(g_code_base + 0x27DABBC, (void*)my_dli_trace, &g_orig_dli, "dli");
    install_post_trace_hook(g_code_base + 0x298F548, (void*)my_dze_trace, &g_orig_dze, "dze");
}

// ==================== GRANT CALLBACK INVOKE PROBE (v4.64) ====================
// Trampoline single-cast delegate invoke @0x2379940:
//   ldr x4,[x0,#0x10]; and w0,w1,#1; mov x1,x2; mov x2,x3; br x4
// Chiamato con x0=delegate, w1=success flag, x2/x3=args. Quando il callback
// catturato da my_dli_trace (g_cap_hdl == [delegate+0x10]) viene invocato con la
// risposta del server, logghiamo success (w1) e gli argomenti x2 (response DTO/obj)
// e x3. Il trampoline e' ENTRY-HOOK: install_post_trace_hook copia 16 byte e fa
// ldr x16,#8;br x16 -> trampoline che riesegue le 4 istruzioni + br x4. Nessun
// adrp nelle prime 4 istruzioni -> fix_adrp_in_trampoline non fa danni.
static void *g_orig_delegate_invoke = nullptr;

static void *my_delegate_invoke_trace(void *x0, void *x1, void *x2, void *x3, unsigned int x4) {
    if (g_cap_hdl && x0) {
        uint64_t hdl = 0;
        safe_read_self((uintptr_t)x0 + 0x10, &hdl, 8);
        if (hdl == (uint64_t)g_cap_hdl) {
            LOGI("CB_INVOKE(0x2379940): matched handler=%p (RVA 0x%llX) w1=0x%x success=%u x2=%p x3=%p",
                 (void*)hdl, (unsigned long long)(hdl - (uint64_t)g_code_base),
                 (unsigned int)(uintptr_t)x1, (unsigned int)(uintptr_t)x1 & 1u, x2, x3);
            maybe_dump_string(x2, "cb.x2");
        }
    }
    return g_orig_delegate_invoke ? ((post_trace_fn)g_orig_delegate_invoke)(x0, x1, x2, x3, x4) : nullptr;
}

static void install_grant_callback_invoke_probe(void) {
    if (!g_code_base) { LOGE("CB_INVOKE: no g_code_base"); return; }
    install_post_trace_hook(g_code_base + 0x2379940, (void*)my_delegate_invoke_trace, &g_orig_delegate_invoke, "delegate");
}

// ==================== FC.DWU + APPLY-GRANT TRACE (v4.65) ====================
// fc.dwu @0x2A7C700 e' il response handler reale della POST di verifica del grant
// (RVA catturato in CB[grant] via dli). Entry:
//   x0 -> x19 (this), w1 -> w21 -> tbz w21,#0x0,0x2a7cb3c (bit0 w1 = success flag)
//   x2 -> x20 -> mov x0,x20; bl 0x3371958 (parse) -> DTO in x21
//   0x2a7c7e4: ldr w8,[x21,#0x10] (Code) ; cmp w8,#0xc8 ; b.ne (NOPata da store bypass #2)
//   0x2a7c878: bl 0x28bc158 (apply-grant) con x0=x22,x1=x23,w2=0xc8,x3=x24,w4,w5,x6,x7
// Primi 16 byte di entrambe: solo sub/stp, nessun pc-relative -> safe install_post_trace_hook.
static void *g_orig_fc_dwu = nullptr;

static void *my_fc_dwu_trace(void *x0, void *x1, void *x2, void *x3, unsigned int x4) {
    uintptr_t ra = (uintptr_t)__builtin_return_address(0);
    LOGI("FC_DWU(0x2A7C700): CALLED BY %p x0=%p w1=0x%x(bit0=%u) x2=%p x3=%p x4=0x%x",
         (void*)ra, x0, (unsigned)(uintptr_t)x1, (unsigned)(uintptr_t)x1 & 1u, x2, x3, x4);
    maybe_dump_string(x2, "fcd.x2");
    maybe_dump_string(x3, "fcd.x3");
    return g_orig_fc_dwu ? ((post_trace_fn)g_orig_fc_dwu)(x0, x1, x2, x3, x4) : nullptr;
}

typedef void *(*apply_grant_fn)(void *x0, void *x1, void *x2, void *x3,
                                void *x4, void *x5, void *x6, void *x7);
static void *g_orig_apply_grant = nullptr;

static void *my_apply_grant_trace(void *x0, void *x1, void *x2, void *x3,
                                  void *x4, void *x5, void *x6, void *x7) {
    uint32_t status = (unsigned)(uintptr_t)x2;
    LOGI("APPLY_GRANT(0x28bc158): CALLED BY %p x0=%p x1=%p status(%%w2)=0x%x x3=%p "
         "w4=0x%x(w29) w5=0x%x(w27) x6=%p x7=%p",
         __builtin_return_address(0), x0, x1, status, x3,
         (unsigned)(uintptr_t)x4, (unsigned)(uintptr_t)x5, x6, x7);
    maybe_dump_string(x3, "ag.x3");
    return g_orig_apply_grant ? ((apply_grant_fn)g_orig_apply_grant)(x0, x1, x2, x3, x4, x5, x6, x7) : nullptr;
}

static void install_fc_dwu_grant_trace_probes(void) {
    if (!g_code_base) { LOGE("FC_DWU: no g_code_base"); return; }
    install_post_trace_hook(g_code_base + 0x2A7C700, (void*)my_fc_dwu_trace, &g_orig_fc_dwu, "fcdwu");
    install_post_trace_hook(g_code_base + 0x28BC158, (void*)my_apply_grant_trace, &g_orig_apply_grant, "applygrant");
}

// ==================== CLUB ACTIVATION BRIDGE (v4.78) ====================
// PROBLEMA: il grant v56 (dsu) NON ha un case per i prodotti club: con acquisto fake
// vip/elite club non vengono MAI attivati localmente (servono la conferma del server,
// mai scattata). ClubController.meu() @0x291ABF8 (il getter del tier) legge SOLO i 4
// campi cache dell'utente: ClubTierCache (+0x3FC), ClubPermanentCache (+0x400),
// ClubExpiryMs (+0x408), ClubNextClaimMs (+0x410) — campi della classe dati raggiunta
// da [UserController+0x28]. Con la risposta del server questi vengono scritti da
// UserController.esj() @0x299DBD8 (sw w1->[x8+0x3FC], str x3->[x8+0x408], strb w2&1->
// [x8+0x400], str x4->[x8+0x410], poi b 0x29905d8 save). Il fix chiama esj() direttamente
// su un acquisto club, attivando il tier localmente e salvando.
//  - esj(UserController this=x0, int tier=w1, bool permanent=w2, long expiry=x3, long nextClaim=x4)
//  - tier: ClubTier.Vip=1, ClubTier.Elite=2 (dump: enum ClubTier @50731)
//  - UserController istanza = [PurchaseController+0x30] (campo `xfm`, dump riga 6892)
//  - productId = Product.definition(id@0x10) = [[Product+0x10]+0x10] (stringa il2cpp:
//    object 16B, length int32 @+0x10, chars utf16 @+0x14) — verifica objdump dsu@0x28b6aac/0x28b6ac8
// Entry dsu@0x28B6924: sub sp / str x30 / stp x28,x27 / stp x26,x25 -> nessun pc-relative
// nei primi 16 byte -> safe install_post_trace_hook (come per fcdwu/applygrant).
static void *g_orig_dsu = nullptr;

// v4.81 (senza bump versione) GUARD anti-reset: quando il club e' stato attivato
// localmente, il server (ClubStatusResponseDTO via mfa@0x291ADE8) risponde tier=0
// al claim / codice Discord e chiama esj(tier=0)+save -> azzera i campi cache
// (+0x3FC/+0x400/+0x408/+0x410) e il club "torna normale". Il wrapper su esj
// (entry safe: str x30 / ldr x8 / cbz x8 / and w9 -> no adrp nei primi 16B)
// intercetta la chiamata: se il club e' attivo e il tier in ingresso e' 0, riscrive
// x1/x2/x3/x4 con lo stato salvato, cosi' il reset server non attecchisce.
// FIRMA dedicata con x0..x4 tutti a 64-bit: post_trace_fn tronca x4 (nextClaim,
// epoch ms ~1.7e12) a 32-bit -> proibito. Trampoline + wrapper preservano i regs.
static int g_club_active = 0;
static int g_club_tier = 0;
static int g_club_permanent = 0;
static int64_t g_club_expiry = 0;
static int64_t g_club_next_claim = 0;
static void *g_orig_esj = nullptr;

typedef void *(*esj_full_fn)(void *x0, void *x1, void *x2, void *x3, void *x4);

static void *my_esj_club_guard(void *x0, void *x1, void *x2, void *x3, void *x4) {
    int incoming = (int)(uintptr_t)x1;
    if (g_club_active && incoming <= 0) {
        LOGI("CLUB_GUARD: esj reset tier=%d BLOCKED -> forcing tier=%d perm=%d expiry=%lld next=%lld",
             incoming, g_club_tier, g_club_permanent,
             (long long)g_club_expiry, (long long)g_club_next_claim);
        x1 = (void*)(uintptr_t)g_club_tier;
        x2 = (void*)(uintptr_t)g_club_permanent;
        x3 = (void*)(uintptr_t)g_club_expiry;
        x4 = (void*)(uintptr_t)g_club_next_claim;
    }
    return g_orig_esj ? ((esj_full_fn)g_orig_esj)(x0, x1, x2, x3, x4) : nullptr;
}

static int64_t club_now_ms(void) {
    struct timespec ts;
    if (clock_gettime(CLOCK_REALTIME, &ts) != 0) return 0;
    return (int64_t)ts.tv_sec * 1000 + ts.tv_nsec / 1000000;
}

// v4.81 (senza bump) PERSISTENZA club tra riavvii: g_club_tier/permanent/expiry/
// next_claim e' solo in RAM -> al rilancio del gioco muore e il server (mfa) con
// tier=0 riazzererebbe i campi (guard inattivo con g_club_active=0). Salviamo lo
// stato in un file nella dir dati del modulo (stessa dir di drmod.log) e lo
// rileggiamo nel worker all'avvio, cosi' il guard blocca il reset anche dopo
// restart. Il file NON viene cancellato da adb rm drmod.log (regola 9l: solo il log).
static const char *CLUB_STATE_PATH    = "/data/data/com.lavalabs.dungeonrush/files/drmod_club.dat";
static const char *CLUB_STATE_PATH_ALT= "/data/user/0/com.lavalabs.dungeonrush/files/drmod_club.dat";

static void club_state_save(void) {
    if (g_club_tier <= 0) { LOGI("CLUB_STATE: skip save (tier=%d)", g_club_tier); return; }
    FILE *f = fopen(CLUB_STATE_PATH, "w");
    if (!f) f = fopen(CLUB_STATE_PATH_ALT, "w");
    if (!f) { LOGE("CLUB_STATE: save fopen failed errno=%d (%s)", errno, strerror(errno)); return; }
    fprintf(f, "%d %d %lld %lld %d\n",
            g_club_tier, g_club_permanent, (long long)g_club_expiry, (long long)g_club_next_claim, g_club_active);
    fclose(f);
    LOGI("CLUB_STATE: saved tier=%d perm=%d expiry=%lld next=%lld active=%d",
         g_club_tier, g_club_permanent, (long long)g_club_expiry, (long long)g_club_next_claim, g_club_active);
}

static void club_state_load(void) {
    g_club_tier = 0; g_club_permanent = 0; g_club_expiry = 0; g_club_next_claim = 0; g_club_active = 0;
    FILE *f = fopen(CLUB_STATE_PATH, "r");
    if (!f) f = fopen(CLUB_STATE_PATH_ALT, "r");
    if (!f) { LOGI("CLUB_STATE: no state file (club not activated yet)"); return; }
    long long e = 0, n = 0; int a = 0;
    int tier = 0, perm = 0;
    int r = fscanf(f, "%d %d %lld %lld %d", &tier, &perm, &e, &n, &a);
    fclose(f);
    if (r != 5 || tier <= 0) { LOGI("CLUB_STATE: file corrupt/empty (r=%d tier=%d)", r, tier); return; }
    g_club_tier = tier; g_club_permanent = perm; g_club_expiry = e; g_club_next_claim = n;
    g_club_active = a ? 1 : 0;
    LOGI("CLUB_STATE: restored tier=%d perm=%d expiry=%lld next=%lld active=%d",
         g_club_tier, g_club_permanent, (long long)g_club_expiry, (long long)g_club_next_claim, g_club_active);
}

static void *my_dsu_club_bridge(void *x0, void *x1, void *x2, void *x3, unsigned int x4) {
    if (x0 && x1 && g_code_base) {
        uint64_t def = 0, idp = 0;
        if (safe_read_self((uintptr_t)x1 + 0x10, &def, 8) && def &&
            safe_read_self(def + 0x10, &idp, 8) && idp) {
            int32_t len = 0;
            if (safe_read_self(idp + 0x10, &len, 4) && len > 0 && len < 64) {
                uint8_t raw[128];
                if (safe_read_self(idp + 0x14, raw, (size_t)len * 2)) {
                    char id[64];
                    int i;
                    for (i = 0; i < len && i < 63; i++) id[i] = (char)raw[i * 2];
                    id[i] = 0;
                    LOGI("CLUB_BRIDGE: productId=\"%s\"", id);
                    int tier = 0;
                    if (strncmp(id, "lootio.vip_club_", 16) == 0) tier = 1;
                    else if (strncmp(id, "lootio.elite_club_", 18) == 0) tier = 2;
                    if (tier) {
                        int permanent = strstr(id, "_permanent") ? 1 : 0;
                        uint64_t uc = 0;
                        if (safe_read_self((uintptr_t)x0 + 0x30, &uc, 8) && uc) {
                            uint64_t data = 0;
                            if (safe_read_self(uc + 0x28, &data, 8) && data) {
                                int64_t now = club_now_ms();
                                int64_t expiry = permanent ? 0 : now + 30LL * 24 * 3600 * 1000;
                                g_club_active = 1;
                                g_club_tier = tier;
                                g_club_permanent = permanent;
                                g_club_expiry = expiry;
                                g_club_next_claim = now;
                                club_state_save();
                                typedef void (*esj_fn)(void *self, int tier, int permanent, int64_t expiry, int64_t nextClaim);
                                esj_fn esj = (esj_fn)(g_code_base + 0x299DBD8);
                                LOGI("CLUB_BRIDGE: ACTIVATE product=\"%s\" tier=%d permanent=%d expiry=%lld via esj@0x299DBD8",
                                     id, tier, permanent, (long long)expiry);
                                esj((void*)uc, tier, permanent, expiry, now);
                            } else LOGE("CLUB_BRIDGE: UserController data @+0x28 null");
                        } else LOGE("CLUB_BRIDGE: no UserController at PC+0x30");
                    }
                }
            }
        }
    }
    return g_orig_dsu ? ((post_trace_fn)g_orig_dsu)(x0, x1, x2, x3, x4) : nullptr;
}

static void install_club_bridge(void) {
    if (!g_code_base) { LOGE("CLUB: no g_code_base"); return; }
    install_post_trace_hook(g_code_base + 0x28B6924, (void*)my_dsu_club_bridge, &g_orig_dsu, "dsu");
    install_post_trace_hook(g_code_base + 0x299DBD8, (void*)my_esj_club_guard, &g_orig_esj, "esj-guard");
}

// ==================== VERIFY IAP (v4.63: server-side validation PROBE + Code=0 FORCE) ====================
// Server-side: PurchaseController -> NetworkController.VerifyIAPRequest(type,transactionId,receipt,
// onComplete) @0x28ad5c4 salva onComplete in wzx [this+0x48], fa POST a verifyinapppurchase-...-uc.
// a.run.app/. La callback risposta e' dnv(HTTPRequest,HTTPResponse) @0x28ad894: legge status=[b+0x18],
// se 200 parsa il body JSON in VerifyIAPResponseDTO (bl 0x3373244 @0x28ad928) e invoca wzx(dto);
// se status!=200 o b=null crea un DTO vuoto col ctor (Code=0 default) e invoca wzx(dto) comunque.
// PROBLEMA: il server rifiuta l'orderId fake "MOD.xxx" rispondendo body {"Code":<err>} con HTTP 200
// => il DTO parsato ha Code!=0 => l'onComplete rifiuta il grant (nessun effetto in game).
// FIX v4.63: patch a 4 byte del `bl 0x3373244` @0x28ad928 -> stub che chiama il parse originale e,
// sul DTO risultante, azzera Code(+0x10) e setta AlreadyVerified(+0x20)/S2STracked(+0x21) a true.
// Stub ARM64 verificata con clang (rkforce_stub.s): nessun clobber oltre caller-saved/x30 gestito.
static void *g_orig_verify_iap = nullptr;
static void *g_orig_dnv = nullptr;

static void *my_verify_iap(void *x0, void *x1, void *x2, void *x3, void *x4) {
    LOGI("VERIFY_IAP(0x28ad5c4): CALLED this=%p type=%p txn=%p receipt=%p onComplete=%p",
         x0, x1, x2, x3, x4);
    maybe_dump_string(x1, "vtype");
    maybe_dump_string(x2, "vtxn");
    maybe_dump_string(x3, "vreceipt");
    return g_orig_verify_iap ? ((void *(*)(void*,void*,void*,void*,void*))g_orig_verify_iap)(x0, x1, x2, x3, x4) : nullptr;
}

static void *my_dnv(void *x0, void *x1, void *x2, void *x3, unsigned int x4) {
    uint32_t status = 0;
    uintptr_t wzx = 0;
    if (x2) safe_read_self((uintptr_t)x2 + 0x18, &status, 4);
    if (x0) safe_read_self((uintptr_t)x0 + 0x48, &wzx, 8);
    LOGI("DNV(0x28ad894) [VerifyIAPResponseCb]: CALLED this=%p req=%p resp=%p status=%u wzx=%p",
         x0, x1, x2, status, (void*)wzx);
    return g_orig_dnv ? ((post_trace_fn)g_orig_dnv)(x0, x1, x2, x3, x4) : nullptr;
}

static void install_verify_iap_force(void) {
    if (!g_code_base) { LOGE("VERIFY_IAP: no g_code_base"); return; }
    uintptr_t off = 0x28AD928;                       // bl 0x3373244 (parse DTO) dentro dnv
    uintptr_t target = g_code_base + off;
    uint32_t orig_bl = 0x942B1647;                   // byte originali verificati = `bl 0x3373244`
    uint32_t read = 0;
    if (!safe_read_self(target, &read, 4) || read != orig_bl) {
        LOGE("VERIFY_IAP force: unexpected bytes @0x28ad928 (got %08x, want %08x) - SKIP", read, orig_bl);
        return;
    }

    void *stub = alloc_exec_near((void*)target, 0x40);
    if (!stub) { LOGE("VERIFY_IAP force: alloc_exec_near failed"); return; }
    uint32_t *s = (uint32_t*)stub;
    static const uint32_t CODE[11] = {
        0xf81f0ffe,   // str x30,[sp,#-0x10]!
        0x58000130,   // ldr x16,[pc,#0x28]        -> literal @stub+0x28
        0xd63f0200,   // blr x16                   (call 0x3373244: x0=json, x1=type => DTO in x0)
        0xb40000a0,   // cbz x0, +0x14             (skip se parse null)
        0xb900101f,   // str wzr,[x0,#0x10]        Code = 0
        0x52800028,   // mov w8,#1
        0x39008008,   // strb w8,[x0,#0x20]        AlreadyVerified = true
        0x39008408,   // strb w8,[x0,#0x21]        S2STracked = true
        0xf84107fe,   // ldr x30,[sp],#0x10        (restore return addr 0x28ad92c)
        0xd65f03c0,   // ret
        0x00000000    // (padding, literal 8B allocato sotto con align)
    };
    for (int i = 0; i < 11; i++) s[i] = CODE[i];
    *(uintptr_t*)((uint8_t*)stub + 0x28) = (uintptr_t)(g_code_base + 0x3373244);  // parse setter
    __builtin___clear_cache((char*)stub, (char*)stub + 0x40);

    int64_t bl_delta = (int64_t)((uintptr_t)stub - target) / 4;
    if (bl_delta < -(1<<25) || bl_delta >= (1<<25)) {
        LOGE("VERIFY_IAP force: bl out of range delta=%lld", (long long)bl_delta);
        return;
    }
    uint32_t bl_ins = 0x94000000 | (bl_delta & 0x3FFFFFF);
    if (proc_mem_write(target, &bl_ins, 4)) {
        __builtin___clear_cache((char*)target, (char*)target + 4);
        LOGI("VERIFY_IAP force: patched bl @0x28ad928 -> stub %p (Code=0 on parsed DTO) readback=%08x",
             stub, *(uint32_t*)target);
    } else {
        LOGE("VERIFY_IAP force: proc_mem_write FAILED at 0x28ad928");
    }
}

static void install_verify_iap_probes(void) {
    if (!g_code_base) { LOGE("VERIFY_IAP probe: no g_code_base"); return; }
    install_post_trace_hook(g_code_base + 0x28AD5C4, (void*)my_verify_iap, &g_orig_verify_iap, "verifyiap");
    install_post_trace_hook(g_code_base + 0x28AD894, (void*)my_dnv, &g_orig_dnv, "dnv");
    install_verify_iap_force();
}

// ==================== FALLBACK ENTRY PROBE (0x28a92e8) ====================
// entry+0x160 (fallback GRANTBL): ldr x8,[x20,#0x10]; cbz x8,0x28a9428; ...
// NON useremo inline_hooker qui: nei 16 byte c'e' un cbz pc-relative che, copiato
// nel backup trampoline da inline_hooker, verrebbe rieseguito alla NUOVA posizione
// con displacement sbagliato -> branch errato. Invece patch a 4 byte (bl), come
// install_x20_gate_probe: la stub logga, poi RISEGUE l'istruzione originale
// (ldr x8,[x20,#0x10]) e fa b a 0x28a92ec (il cbz nativo successivo). Cosi' il
// flusso nativo continua identico. Stub: dxn CODE[70] (gia' verificata) + resume.
static void grant_probe_fallback_logger(void) {
    LOGI("GRANT_CHAIN_fallback: ENTRY @0x28a92e8 (fallback GRANTBL, patched 4-byte bl)");
}

static void install_fallback_entry_probe(void) {
    if (!g_code_base) { LOGE("fallback probe: no code_base (store hook failed)"); return; }
    uintptr_t off = 0x28A92E8;
    uintptr_t target = g_code_base + off;
    void *stub = alloc_exec_near((void*)target, 0x148);
    if (!stub) { LOGE("fallback probe: alloc_exec_near failed"); return; }
    uint32_t *s = (uint32_t*)stub;

    static const uint32_t CODE[70] = {
        0xd11403ff,0xa90007e0,0xa9010fe2,0xa90217e4,0xa9031fe6,0xa90427e8,0xa9052fea,0xa90637ec,0xa9073fee,0xa90847f0,
        0xa9094ff2,0xa90a57f4,0xa90b5ff6,0xa90c67f8,0xa90d6ffa,0xa90e77fc,0xf9007bfe,0xad0807e0,0xad090fe2,0xad0a17e4,
        0xad0b1fe6,0xad0c27e8,0xad0d2fea,0xad0e37ec,0xad0f3fee,0xad1047f0,0xad114ff2,0xad1257f4,0xad135ff6,0xad1467f8,
        0xad156ffa,0xad1677fc,0xad177ffe,0x580004b0,0xd63f0200,0xad577ffe,0xad5677fc,0xad556ffa,0xad5467f8,0xad535ff6,
        0xad5257f4,0xad514ff2,0xad5047f0,0xad4f3fee,0xad4e37ec,0xad4d2fea,0xad4c27e8,0xad4b1fe6,0xad4a17e4,0xad490fe2,
        0xad4807e0,0xf9407bfe,0xa94e77fc,0xa94d6ffa,0xa94c67f8,0xa94b5ff6,0xa94a57f4,0xa9494ff2,0xa94847f0,0xa9473fee,
        0xa94637ec,0xa9452fea,0xa94427e8,0xa9431fe6,0xa94217e4,0xa9410fe2,0xa94007e0,0x911403ff,0x58000090,0xd61f0200
    };
    for (int i = 0; i < 70; i++) s[i] = CODE[i];
    *(uintptr_t*)((uint8_t*)stub + 0x118) = (uintptr_t)(uintptr_t)grant_probe_fallback_logger;

    // Resume block after CODE[70] (offset 0x128): ri-esegue l'istruzione originale
    // ldr x8,[x20,#0x10] (0xf9400a88, dal disasm verif. dal GRANTBL dump a runtime)
    // e salta al cbz nativo @0x28a92ec. Salto con ldr x16,[pc,#8]/br x16 (idioma
    // gia' verificato di inline_hooker, literal quad a 8-align) => NESSUN vincolo
    // di range: il vecchio `b imm26` falliva con stub a -55MB (check errato ±32MB).
    // 0x120 viene usato da CODE[68] (ldr x16,@0x120) come ingresso del resume.
    uint8_t *resume = (uint8_t*)stub + 0x128;
    *(uint32_t*)(resume + 0x00) = 0xf9400a88;   // ldr x8,[x20,#0x10]  (istruzione originale)
    *(uint32_t*)(resume + 0x04) = 0xd503201f;   // nop (allinea il literal a 8 byte)
    *(uint32_t*)(resume + 0x08) = 0x58000050;   // ldr x16,[pc,#8]     -> literal @resume+0x10
    *(uint32_t*)(resume + 0x0c) = 0xd61f0200;   // br x16
    *(uintptr_t*)(resume + 0x10) = (uintptr_t)(g_code_base + 0x28A92EC); // cbz nativo
    *(uintptr_t*)((uint8_t*)stub + 0x120) = (uintptr_t)resume;
    __builtin___clear_cache((char*)stub, (char*)stub + 0x148);

    int64_t bl_delta = (int64_t)((uintptr_t)stub - target) / 4;
    if (bl_delta < -(1<<25) || bl_delta >= (1<<25)) {
        LOGE("fallback probe: bl out of range delta=%lld", (long long)bl_delta);
        return;
    }
    uint32_t bl_ins = 0x94000000 | (bl_delta & 0x3FFFFFF);
    if (proc_mem_write(target, &bl_ins, 4)) {
        __builtin___clear_cache((char*)target, (char*)target + 4);
        LOGI("fallback probe: patched ldr-x8@0x28a92e8 -> bl %p (log-only, resume->0x28a92ec)", stub);
        LOGI("fallback probe: readback=%08x", *(uint32_t*)target);
    } else {
        LOGE("fallback probe: proc_mem_write FAILED at 0x28a92e8");
    }
}

// ==================== TRACCIATURA COMPLETA 0x28aea94 (processing intermedio) ====================
// 0x28aea94 = "proc00" = funzione che ProcessPurchase chiama a 0x28aef88 quando il fallback
// va sul path che DEVE erogare (returned 1). DENTRO proc00 ci sono molti cbz che possono
// fare early-exit a 0x28aec2c PRIMA del `bl 0x28af04c` (28aec08, l'handler che eroga).
// Il log della 4.34 mostra che `VERIFICA VALIDATOR` (dentro 0x28af04c) NON scatta => proc00
// esce presto. Per scoprire QUALE gate blocca, tracciamo l'INGRESSO di proc00 loggando
// tutti i valori che determinano i cbz: x0=self, x1=args, x2=component, [args+0x18],
// [self+0x30]=product. Con questi sapremo dov'e' il blocco senza analizzare staticamente.
// proc00 = 0x28aea94 = entry(0x28aed00) - 0x26C.
typedef int (*proc00_fn)(void *self, void *args, void *component);
static proc00_fn g_orig_proc00 = nullptr;
static uintptr_t g_proc00_entry = 0;

static void log_proc00_state(uintptr_t self, uintptr_t args, uintptr_t component,
                             uintptr_t args8, uintptr_t args10, uintptr_t args18, uintptr_t self30) {
    LOGI("PROC00 STATE: self=0x%llx args=0x%llx component=0x%llx",
         (unsigned long long)self, (unsigned long long)args, (unsigned long long)component);
    LOGI("PROC00 STATE:   [args+0x8]=0x%llx %s | [args+0x10]=0x%llx %s",
         (unsigned long long)args8, args8 ? "NON-NULL" : "NULL",
         (unsigned long long)args10, args10 ? "NON-NULL" : "NULL ** blocco cbz 28aeb70 **");
    LOGI("PROC00 STATE:   [args+0x18]=0x%llx %s | [self+0x30](product)=0x%llx %s",
         (unsigned long long)args18, args18 ? "NON-NULL" : "NULL ** blocco cbz 28aeb30 **",
         (unsigned long long)self30, self30 ? "NON-NULL" : "NULL ** blocco cbz 28aebe4 **");
}

// Legge in modo sicuro [base+off]; se base e' null o la regione non e' leggibile ritorna 0.
static uintptr_t proc00_deref(uintptr_t base, uintptr_t off) {
    if (!base) return 0;
    uintptr_t v = 0;
    if (safe_read_self(base + off, &v, sizeof(v))) return v;
    return 0;
}

static int my_proc00(void *self, void *args, void *component) {
    uintptr_t sb = (uintptr_t)self;
    uintptr_t ab = (uintptr_t)args;
    uintptr_t self30 = proc00_deref(sb, 0x30);
    uintptr_t args18 = proc00_deref(ab, 0x18);
    uintptr_t args8  = proc00_deref(ab, 0x08);
    uintptr_t args10 = proc00_deref(ab, 0x10);
    LOGI("PROC00 ENTRY: called self=%p args=%p component=%p", self, args, component);
    log_proc00_state(sb, ab, (uintptr_t)component, args8, args10, args18, self30);
    if (g_orig_proc00) {
        int r = g_orig_proc00(self, args, component);
        LOGI("PROC00 RET: orig returned %d (not 0x30 chain => early-exit se diverso da path erogazione)", r);
        return r;
    }
    return 0;
}

static void install_proc00_trace(void) {
    if (!g_proc_purchase_entry) { LOGE("proc00 trace: g_proc_purchase_entry not set"); return; }
    g_proc00_entry = g_proc_purchase_entry - 0x26C;   // 0x28aea94
    uintptr_t orig = g_proc00_entry;
    LOGI("proc00 trace: targeting 0x28aea94 (entry %p)", (void*)orig);
    void *trampoline = alloc_exec_near((void*)orig, 64);
    if (!trampoline) { LOGE("proc00 trace: alloc_exec_near trampoline failed"); return; }
    memcpy(trampoline, (void*)orig, 16);
    fix_adrp_in_trampoline(trampoline, (void*)orig);
    uint32_t *t = (uint32_t*)trampoline;
    t[4] = 0x58000050;
    t[5] = 0xD61F0200;
    *(uint64_t*)&t[6] = (uint64_t)orig + 16;
    __builtin___clear_cache((char*)trampoline, (char*)trampoline + 64);
    uint8_t jmp[16];
    *(uint32_t*)&jmp[0] = 0x58000050;
    *(uint32_t*)&jmp[4] = 0xD61F0200;
    *(uint64_t*)&jmp[8] = (uint64_t)my_proc00;
    if (proc_mem_write((uintptr_t)orig, jmp, 16)) {
        __builtin___clear_cache((char*)orig, (char*)orig + 16);
        if (memcmp((void*)orig, jmp, 16) == 0) {
            g_orig_proc00 = (proc00_fn)trampoline;
            LOGI("proc00 trace: inline hook OK (func=0x28aea94, trampoline=%p)", (void*)trampoline);
        } else {
            LOGE("proc00 trace: inline hook VERIFY FAILED — patch did not stick");
        }
    } else {
        LOGE("proc00 trace: proc_mem_write FAILED at 0x28aea94");
    }
}

// Probe sui gate interni di 0x28aea94 che possono fare early-exit a 0x28aec2c (blocco).
// Loggiamo il valore che determina il gate e poi RIPRODUCIAMO la decisione originale,
// cosi' la logica resta intatta e sappiamo esattamente QUALE gate scatta.
//   kind 0 = cbz x21 (args)                  [offset 0x94]
//   kind 1 = cbz [x21+0x18] (field args)     [offset 0x9C]
//   kind 2 = cbz x20 (product, [self+0x30])  [offset 0x150]
// NOTA: len reg reg offset di patching dei byte viene dichiarato come intero non usato.
static void log_cbz_gate(int kind, uintptr_t v) {
    const char *name = (kind==2)?"x20(product,[self+0x30])":(kind==1)?"[x21+0x18](field args)":"x21(args)";
    LOGI("PROC00 CBZ[%d]: %s = 0x%llx %s", kind, name, (unsigned long long)v,
         v ? "NON-NULL => gate OK, continua" : "NULL => EARLY-EXIT 0x28aec2c ** questo gate BLOCCA **");
}

static void install_cbz_probe(uintptr_t proc00, uint32_t off, int kind, const char *name, uintptr_t early) {
    uintptr_t target = proc00 + off;
    void *stub = alloc_exec_near((void*)target, 160);   // 0xA0
    if (!stub) { LOGE("cbz probe[%s]: alloc_exec_near stub failed", name); return; }
    uint32_t *s = (uint32_t*)stub;
    // log_cbz_gate(int kind, uintptr_t v): x0=kind, x1=v
    // DESIGN ROBUSTO: NON preserviamo il gate in un registro a parte attraverso la call C
    // (che corrompeva lo stack-slot e portava il `ret` a un indirizzo spazzatura => SIGBUS
    // a pc=0x12d in 4.39). Il gate viene RICALCOLATO dopo i pop, dall'istanza preservata:
    //   kind0: mov x27, x21          (args)
    //   kind1: ldr x27,[x21,#0x18]   (field args) + mov x8,x27
    //   kind2: mov x27, x20          (product)
    // Prima della call C usiamo x27 per l'argomento x1. Tutti i pop sono LIFO corretti.
    // Ist: 0x00..0x5f, dati logger@0x68, early@0x70. size 128 (0x80).
    s[0x00/4] = 0xa9bf7be0; // stp x0, x30, [sp, #-0x10]!
    s[0x04/4] = 0xa9bf0be1; // stp x1, x2, [sp, #-0x10]!
    s[0x08/4] = 0xa9bf1be3; // stp x3, x4, [sp, #-0x10]!
    s[0x0c/4] = 0xa9bf53f5; // stp x20, x21, [sp, #-0x10]!
    s[0x10/4] = 0xa9bf5bf7; // stp x22, x23, [sp, #-0x10]!
    s[0x14/4] = 0xa9bf63f9; // stp x24, x25, [sp, #-0x10]!
    s[0x18/4] = 0xa9bf6bfb; // stp x26, x27, [sp, #-0x10]!
    s[0x1c/4] = (kind==2) ? 0xaa1403fb : (kind==1) ? 0xaa0803fb : 0xaa1503fb; // mov x27, gate-reg (64-bit)
    s[0x20/4] = 0x52800000 | ((uint32_t)kind << 5); // mov w0, #<kind>
    s[0x24/4] = 0xaa1b03e1;                  // mov x1, x27  (v)
    s[0x28/4] = 0x58000250;                  // ldr x16, [pc, #0x48] -> literal @0x70 (logger)
    s[0x2c/4] = 0xd63f0200;                  // blr x16     (call log_cbz_gate)
    s[0x30/4] = 0xa8c16bfb;                  // ldp x26, x27, [sp], #0x10
    s[0x34/4] = 0xa8c163f9;                  // ldp x24, x25, [sp], #0x10
    s[0x38/4] = 0xa8c15bf7;                  // ldp x22, x23, [sp], #0x10
    s[0x3c/4] = 0xa8c153f5;                  // ldp x20, x21, [sp], #0x10
    s[0x40/4] = 0xa8c11be3;                  // ldp x3, x4, [sp], #0x10
    s[0x44/4] = 0xa8c10be1;                  // ldp x1, x2, [sp], #0x10
    s[0x48/4] = 0xa8c17be0;                  // ldp x0, x30, [sp], #0x10
    // RICALCOLA gate in x27 dall'istanza preservata (dopo tutti i pop).
    if (kind==1) {
        s[0x4c/4] = 0xf9400ebb;              // ldr x27, [x21, #0x18]  (rilegge field args)
        s[0x50/4] = 0xaa1b03e8;              // mov x8, x27            (ripristina x8!)
    } else {
        s[0x4c/4] = (kind==2) ? 0xaa1403fb : 0xaa1503fb; // mov x27, x20 / mov x27, x21
        s[0x50/4] = 0xd503201f;              // nop
    }
    s[0x54/4] = 0xb400005b;                  // cbz x27, #+0x8 -> 0x5c (early) se x27==0
    s[0x58/4] = 0xd65f03c0;                  // ret (x27!=0: x30=entry+4 => continua proc00)
    s[0x5c/4] = 0x580000f0;                  // ldr x16, [pc, #0x1c] -> literal @0x78 (early)
    s[0x60/4] = 0xd61f0200;                  // br x16
    s[0x64/4] = 0xd503201f;                  // nop
    s[0x68/4] = 0xd503201f;                  // nop
    s[0x6c/4] = 0xd503201f;                  // nop
    *(uint64_t*)((uint8_t*)stub + 0x70) = (uint64_t)(uintptr_t)log_cbz_gate; // literal logger
    *(uint64_t*)((uint8_t*)stub + 0x78) = (uint64_t)early;                    // literal early target
    __builtin___clear_cache((char*)stub, (char*)stub + 128);

    int64_t delta = (int64_t)((uintptr_t)stub - target) / 4;
    if (delta < -(1<<25) || delta >= (1<<25)) { LOGE("cbz probe[%s]: bl out of range", name); return; }
    uint32_t bl_ins = 0x94000000 | (delta & 0x3FFFFFF);
    if (proc_mem_write(target, &bl_ins, 4)) {
        __builtin___clear_cache((char*)target, (char*)target + 4);
        LOGI("cbz probe[%s]: patched cbz@0x%lx -> bl %p (gate observer, logic intact)", name, (unsigned long)target, stub);
    } else {
        LOGE("cbz probe[%s]: proc_mem_write FAILED at 0x%lx", name, (unsigned long)target);
    }
}

// ==================== NOP GATE NON TRACCIATI in 0x28aea94 ====================
// Diag 4.40: i 3 cbz tracciati (0x94, 0x9C, 0x150) passano tutti NON-NULL, ma il flusso
// non arriva mai al handler 0x28af04c. I 5 cbz INTERMEDI non tracciati (0xD4, 0xDC, 0xE4,
// 0x108, 0x110) - che dipendono da x19/qargs creati dentro proc00 e non osservati -
// sono i candidati che fanno early-exit a 0x28aec2c. Li trasformiamo in NOP per forzare
// il flusso fino al bl 0x28af04c. NOP su cbz = branch-over (non prende il salto).
static void nop_untraced_gates(void) {
    if (!g_proc00_entry) { LOGE("nop_untraced_gates: g_proc00_entry not set"); return; }
    const uint32_t nop = 0xd503201f; // nop (ARM64)
    // Offset (from proc00 entry 0x28aea94) dei 5 cbz intermedi non tracciati.
    // CBZ[1] e' a 0x9C, CBZ[2] a 0x150; tra questi ci sono le gate 0xD4/0xDC/0xE4/0x108/0x110.
    const uintptr_t offs[] = { 0xD4, 0xDC, 0xE4, 0x108, 0x110 };
    int done = 0;
    for (size_t i = 0; i < sizeof(offs) / sizeof(offs[0]); i++) {
        uintptr_t a = g_proc00_entry + offs[i];
        if (proc_mem_write(a, &nop, sizeof(nop))) {
            LOGI("nop_untraced_gates: NOP cbz@0x%lx", (unsigned long)offs[i]);
            done++;
        } else {
            LOGE("nop_untraced_gates: proc_mem_write FAILED cbz@0x%lx", (unsigned long)offs[i]);
        }
    }
    LOGI("nop_untraced_gates: DONE %d/%d gate NOPed", done, (int)(sizeof(offs) / sizeof(offs[0])));
}

// ==================== TRACCIA HANDLER 0x28af04c (completamento) ====================
// Se il flusso arriva qui (dopo aver superato i gate interni di 0x28aea94), l'handler
// di completamento viene chiamato: logga l'ingresso per confermare che il path di
// erogazione e' stato raggiunto fino a questo punto. 0x28af04c = entry(0x28aed00)+0x34C.
typedef int (*proc01_fn)(void *x0, void *x1, void *x2, void *x3, void *x4, void *x5, void *x6);
static proc01_fn g_orig_proc01 = nullptr;
static uintptr_t g_proc01_entry = 0;

static int my_proc01(void *x0, void *x1, void *x2, void *x3, void *x4, void *x5, void *x6) {
    LOGI("PROC01 ENTRY (handler completamento 0x28af04c REACHED): x0=%p x1=%p x2=%p", x0, x1, x2);
    if (g_orig_proc01) return g_orig_proc01(x0, x1, x2, x3, x4, x5, x6);
    return 0;
}

static void install_handler_trace(void) {
    if (!g_proc_purchase_entry) { LOGE("handler trace: g_proc_purchase_entry not set"); return; }
    g_proc01_entry = g_proc_purchase_entry + 0x34C;   // 0x28af04c (= entry+0x34C, NOT 0x2E4!)
    uintptr_t orig = g_proc01_entry;
    void *trampoline = alloc_exec_near((void*)orig, 64);
    if (!trampoline) { LOGE("handler trace: alloc_exec_near trampoline failed"); return; }
    memcpy(trampoline, (void*)orig, 16);
    fix_adrp_in_trampoline(trampoline, (void*)orig);
    uint32_t *t = (uint32_t*)trampoline;
    t[4] = 0x58000050;
    t[5] = 0xD61F0200;
    *(uint64_t*)&t[6] = (uint64_t)orig + 16;
    __builtin___clear_cache((char*)trampoline, (char*)trampoline + 64);
    uint8_t jmp[16];
    *(uint32_t*)&jmp[0] = 0x58000050;
    *(uint32_t*)&jmp[4] = 0xD61F0200;
    *(uint64_t*)&jmp[8] = (uint64_t)my_proc01;
    if (proc_mem_write((uintptr_t)orig, jmp, 16)) {
        __builtin___clear_cache((char*)orig, (char*)orig + 16);
        if (memcmp((void*)orig, jmp, 16) == 0) {
            g_orig_proc01 = (proc01_fn)trampoline;
            LOGI("handler trace: inline hook OK (func=0x28af04c, trampoline=%p)", (void*)trampoline);
        } else {
            LOGE("handler trace: inline hook VERIFY FAILED — patch did not stick");
        }
    } else {
        LOGE("handler trace: proc_mem_write FAILED at 0x28af04c");
    }
}

// dw.ProcessPurchase esce PRIMA del dispatch handler (entry+0x108) se uno dei cbz
// precedenti scatta: entry+0x8c (cbz x21, args null), entry+0x90 (cbz x20, componente
// null), entry+0xa0 (cbz x22, handler null). Il probe handler a entry+0x10c NON scatta,
// quindi ProcessPurchase prende un early-exit. x21(args) e' non-null => il gate
// sospetto e' cbz x20 a entry+0x90 (x20 = risultato di bl da [[0x5a59000+0x5a8]])
// che va a entry+0x2a0 (no grant). Questo probe logga x20 e replica il cbz:
//   x20==0 -> br entry+0x2a0 (early exit)
//   x20!=0 -> ret (entry+0x94, continua ProcessPurchase)
static void log_component(uint64_t comp) {
    LOGI("PROC_PURCH_GATE: x20 (component) = 0x%llx %s",
         (unsigned long long)comp,
         comp ? "NON-NULL => passa cbz x20, continua" : "NULL => EARLY EXIT entry+0x2a0 (NO handler dispatch, NO grant)");
}

static void install_x20_gate_probe(void) {
    if (!g_proc_purchase_entry) { LOGE("gate x20 probe: g_proc_purchase_entry not set (store hook failed)"); return; }
    uintptr_t target = g_proc_purchase_entry + 0x90;        // cbz x20, #+0x210 (early exit)
    void *stub = alloc_exec_near((void*)target, 104);   // 0x68 bytes
    if (!stub) { LOGE("gate x20 probe: alloc_exec_near stub failed"); return; }
    uint32_t *s = (uint32_t*)stub;

    s[0x00/4] = 0xa9bf7be0; // stp x0, x30, [sp, #-0x10]!
    s[0x04/4] = 0xa9bf0be1; // stp x1, x2, [sp, #-0x10]!
    s[0x08/4] = 0xa9bf53e3; // stp x3, x20, [sp, #-0x10]!
    s[0x0c/4] = 0xaa1403fb; // mov x27, x20           (salva componente, 64-bit NON tronca)
    s[0x10/4] = 0xaa1b03e0; // mov x0, x27            (arg = componente, 64-bit)
    s[0x14/4] = 0x58000130; // ldr x16, [pc, #0x24]    -> literal @0x38
    s[0x18/4] = 0xd63f0200; // blr x16                 (call log_component)
    s[0x1c/4] = 0xd503201f; // nop (x0 non e' input del flusso; log_component usa w0/consume) -- mantiene allineamento
    s[0x20/4] = 0xa8c153e3; // ldp x3, x20, [sp], #0x10
    s[0x24/4] = 0xa8c10be1; // ldp x1, x2, [sp], #0x10
    s[0x28/4] = 0xa8c17be0; // ldp x0, x30, [sp], #0x10
    *(uint64_t*)((uint8_t*)stub + 0x38) = (uint64_t)(uintptr_t)log_component; // literal logger
    s[0x34/4] = 0xd503201f; s[0x40/4] = 0xd503201f; s[0x44/4] = 0xd503201f; // nop pad
    s[0x2c/4] = 0xb40000d4; // cbz x20, #+0x18  -> 0x44 (early exit) se x20==0
    s[0x30/4] = 0xd65f03c0; // ret (x30=entry+0x94 => continua ProcessPurchase)
    s[0x44/4] = 0x580000f0; // ldr x16, [pc, #0x1c]    -> literal @0x60
    s[0x48/4] = 0xd61f0200; // br x16
    s[0x4c/4] = 0xd503201f; s[0x50/4] = 0xd503201f; s[0x54/4] = 0xd503201f; // nop
    s[0x58/4] = 0xd503201f; s[0x5c/4] = 0xd503201f; // nop
    *(uint64_t*)((uint8_t*)stub + 0x60) = (uint64_t)(g_proc_purchase_entry + 0x2a0); // literal early-exit target
    __builtin___clear_cache((char*)stub, (char*)stub + 104);

    int64_t delta = (int64_t)((uintptr_t)stub - target) / 4;
    if (delta < -(1<<25) || delta >= (1<<25)) {
        LOGE("gate x20 probe: bl out of range delta=%lld", (long long)delta); return;
    }
    uint32_t bl_ins = 0x94000000 | (delta & 0x3FFFFFF);
    if (proc_mem_write(target, &bl_ins, 4)) {
        __builtin___clear_cache((char*)target, (char*)target + 4);
        LOGI("gate x20 probe: patched cbz-x20@entry+0x90 -> bl %p (x20 gate observer)", stub);
        LOGI("gate x20 probe: initial cbz bytes verified: %08x", *(uint32_t*)target);
    } else {
        LOGE("gate x20 probe: proc_mem_write FAILED at entry+0x90");
    }
}

// Verifica via /proc/self/maps che [addr, addr+len) sia coperto da una regione
// con permesso di lettura. Il modulo gira nel processo target, quindi se la
// regione esiste possiamo leggere direttamente con memcpy (come fa proc_mem_write
// per le scritture). /proc/self/mem NON funziona: il gioco ha dumpable=0 (anti
// debug) e pread fallisce con EIO ancora prima del parsing della catena puntatori.
// maps rimane leggibile dal processo stesso.
static bool self_range_readable(uintptr_t addr, size_t len) {
    uintptr_t end = addr + len;
    FILE *f = fopen("/proc/self/maps", "r");
    if (!f) return false; // se maps non e' leggibile, non rischiamo memcpy (evita SEGV)
    char line[512];
    bool ok = false;
    while (fgets(line, sizeof(line), f)) {
        uintptr_t rstart, rend;
        char perms[8] = {0};
        if (sscanf(line, "%llx-%llx %7s", (unsigned long long*)&rstart,
                   (unsigned long long*)&rend, perms) != 3) continue;
        if (addr < rstart) continue;
        if (end > rend) continue;
        if (perms[0] == 'r') { ok = true; break; }
    }
    fclose(f);
    return ok;
}

// Legge memoria del proprio processo con memcpy diretto, dopo aver verificato
// la regione su /proc/self/maps (niente segfault su indirizzi non mappati).
static bool safe_read_self(uintptr_t addr, void *buf, size_t len) {
    if (!addr || !len) return false;
    if (!self_range_readable(addr, len)) return false;
    memcpy(buf, (void*)addr, len);
    return true;
}

// LOGGERS: printa un buffer come ascii inline + hex; evita caratteri strani.
static void dump_hbuf(const char *tag, uintptr_t addr, size_t len) {
    unsigned char b[80];
    if (len > sizeof(b)) len = sizeof(b);
    if (!safe_read_self(addr, b, len)) {
        LOGI("HNAME %s @%p: read FAIL", tag, (void*)addr);
        return;
    }
    char hex[256], asc[80];
    for (size_t i = 0; i < len; i++) {
        sprintf(hex + i*3, "%02x ", b[i]);
        asc[i] = (b[i] >= 0x20 && b[i] < 0x7f) ? (char)b[i] : '.';
    }
    if (len*3 > 0) hex[len*3-1] = 0;
    asc[len] = 0;
    LOGI("HNAME %s @%p (%zuB): %s | %s", tag, (void*)addr, len, hex, asc);
}

// Il metodo handler di [dw+0x50] viene risolto dal nome in un singleton il2cpp.
// Disassembly 4.29: entry+0xa4 adrp x10,%hi(0x5a59000); entry+0xac ldr x10,[x10,#0x560]
// => slot = base + 0x5a59000 + 0x560 = base + 0x5a59560 (NON 0x5a55560 come 4.28).
// entry+0xb4 ldr x1,[x10] => p1 e' il puntatore al nome, [p1] = Il2CppString.
// Oggetto creato da ProcessPurchase: entry+0x10 ldr x22,[x22,#0x5a8] => base+0x5a595a8.
// Tipo usato da F2 (0x54597bc): adrp 0x5a52000 + ldr #0x968 => base+0x5a52968.
static void dump_handler_method_name(uintptr_t base) {
    uintptr_t slot = base + 0x5a59560;
    uint64_t p1 = 0, p2 = 0;
    if (!safe_read_self(slot, &p1, 8)) { LOGI("HNAME slot @%p: read FAIL", (void*)slot); return; }
    LOGI("HNAME slot @%p -> p1 = 0x%llx", (void*)slot, (unsigned long long)p1);
    if (!p1) return;
    if (!safe_read_self((uintptr_t)p1, &p2, 8)) { LOGI("HNAME p1 @%p: read FAIL", (void*)p1); return; }
    LOGI("HNAME p1 deref -> p2 = 0x%llx", (unsigned long long)p2);
    if (!p2) return;
    dump_hbuf("p2+0x10 (obj+0x10)", (uintptr_t)p2 + 0x10, 32);
    dump_hbuf("p2+0x14 (utf16 alt?)", (uintptr_t)p2 + 0x14, 32);
    dump_hbuf("p1+0x10", (uintptr_t)p1 + 0x10, 32);
    dump_hbuf("slot+0x8", slot + 0x8, 16);
}

// Dump dei tipi usati dal grant path: classe oggetto creato (singleton base+0x5a595a8,
// cross-reference da entry+0x10) e classe del dispatch F2 (adrp 0x5a52000+ldr #0x968
// = base+0x5a52968). p1 = holder della classe, [p1] = Il2CppClass*. In Il2CppClass
// name a +0x10 (const char*) e namespaze a +0x18: leggo e stampo le stringhe.
static void dump_purchase_types(uintptr_t base) {
    uintptr_t sidx[2] = { base + 0x5a595a8, base + 0x5a52968 };
    const char *snam[2] = { "OBJCLASS", "F2CLASS" };
    for (int i = 0; i < 2; i++) {
        uint64_t p1 = 0, p2 = 0;
        if (!safe_read_self(sidx[i], &p1, 8)) { LOGI("%s slot @%p: read FAIL", snam[i], (void*)sidx[i]); continue; }
        LOGI("%s slot @%p -> p1 holder = 0x%llx", snam[i], (void*)sidx[i], (unsigned long long)p1);
        if (!p1) continue;
        if (!safe_read_self((uintptr_t)p1, &p2, 8)) { LOGI("%s p1 @%p: read FAIL", snam[i], (void*)p1); continue; }
        LOGI("%s klass = 0x%llx", snam[i], (unsigned long long)p2);
        if (!p2) continue;
        for (int off = 0x10; off <= 0x28; off += 8) {
            uint64_t sp = 0;
            if (!safe_read_self((uintptr_t)p2 + off, &sp, 8)) continue;
            char tmp[96];
            size_t n = 0;
            for (n = 0; n < 80 && sp; n++) {
                uint8_t c = 0;
                if (!safe_read_self((uintptr_t)sp + n, &c, 1)) break;
                if (c == 0) break;
                if (c < 0x20 || c > 0x7e) break;
                tmp[n] = (char)c;
            }
            tmp[n] = 0;
            if (n > 0) LOGI("%s klass+0x%X = 0x%llx '%s'", snam[i], off, (unsigned long long)sp, tmp);
        }
        dump_hbuf(snam[i], (uintptr_t)p2, 48);
    }
}

// Grant async chiamato dal path di grant (entry+0x158). Loggo i 4 byte originali
// dell'istruzione per confermare a runtime che il path venga realmente raggiunto.
static void dump_grant_bl(uintptr_t entry) {
    if (!entry) { LOGE("grant bl: entry null"); return; }
    for (uintptr_t off = 0x148; off <= 0x160; off += 4) {
        uint32_t ins = 0;
        if (safe_read_self(entry + off, &ins, 4)) {
            LOGI("GRANTBL @entry+0x%X: %08x", (unsigned)(off), ins);
        } else {
            LOGI("GRANTBL @entry+0x%X: read FAIL", (unsigned)(off));
        }
    }
}

typedef void (*google_pu_fn)(void *self, void *billingResult, void *purchases);
static google_pu_fn g_orig_google_pu = nullptr;
static void my_google_pu(void *self, void *br, void *purchases) {
    LOGI("STORE_STEP: my_google_pu ENTRY called by %p self=%p br=%p list=%p", __builtin_return_address(0), self, br, purchases);
    LOGI("STORE_STEP: my_google_pu: g_orig_google_pu=%p, will CALL ORIG and then next expected is dw.ProcessPurchase (my_proc_purchase) at %p", (void*)g_orig_google_pu, (void*)my_proc_purchase);
    if (g_orig_google_pu) g_orig_google_pu(self, br, purchases);
    LOGI("STORE_STEP: my_google_pu EXIT orig returned -> next is dw.ProcessPurchase (my_proc_purchase) should trigger within 1s");
    LOGI("STORE_STEP: my_google_pu: if my_proc_purchase NOT triggered, then Google listener not forwarding to dw");
}

// ==================== LSPlant callback dispatcher ====================

static jstring g_product_id = nullptr;

static jclass g_cls_zzbm = nullptr; // deprecated, Play Billing <8.3.0, kept optional
static jclass g_cls_bci = nullptr;
static jclass g_cls_proxy = nullptr;
static jclass g_cls_gpbl_proxy = nullptr; // com.unity3d.services.store.gpbl.proxies.PurchasesResponseListenerProxy
static jclass g_cls_pu_proxy = nullptr; // com.unity3d.services.store.gpbl.proxies.PurchaseUpdatedListenerProxy
static jclass g_cls_max = nullptr;
static jclass g_cls_purchase = nullptr;
static jclass g_cls_zzcj = nullptr; // deprecated zzdc, kept optional
static jclass g_cls_billingresult = nullptr; // com/android/billingclient/api/BillingResult (v8.3.0+)
static jclass g_cls_billingbuilder = nullptr; // BillingResult$Builder
static jclass g_cls_productdetails = nullptr; // com/android/billingclient/api/ProductDetails
static jclass g_cls_queryresult = nullptr; // com/android/billingclient/api/QueryProductDetailsResult
static jclass g_cls_list = nullptr;
static jclass g_cls_arraylist = nullptr;
static jclass g_cls_intent = nullptr;
static jclass g_cls_jsonobject = nullptr;
static jclass g_cls_system = nullptr;
static jclass g_cls_boolean = nullptr;   // java/lang/Boolean (per boxed return)
static jmethodID g_mid_purchase_init = nullptr;
static jmethodID g_mid_list_add = nullptr;
static jmethodID g_mid_arraylist_init = nullptr;
static jmethodID g_mid_intent_putextra_int = nullptr;
static jmethodID g_mid_intent_putsal = nullptr;
static jmethodID g_mid_forward = nullptr;
static jmethodID g_mid_jsonobj_init = nullptr;
static jmethodID g_mid_jsonobj_put = nullptr;
static jmethodID g_mid_system_time = nullptr;
static jmethodID g_mid_boolean_valueof = nullptr;
static jfieldID g_fid_zzcj_zzl = nullptr; // deprecated
static jmethodID g_mid_billing_newbuilder = nullptr;
static jmethodID g_mid_builder_setResponseCode = nullptr;
static jmethodID g_mid_builder_setDebugMessage = nullptr;
static jmethodID g_mid_builder_build = nullptr;

static jobject g_backup_a = nullptr;   // gpbl PurchasesResponseListenerProxy.onQueryPurchasesResponse
static jobject g_backup_b = nullptr;   // (unused, Play Billing 8.3.0 zzbm hooks removed)
static jobject g_backup_c = nullptr;   // (unused, Play Billing 8.3.0 zzaI hook removed)
static jobject g_backup_d = nullptr;   // BillingClientImpl.queryProductDetailsAsync
static jobject g_backup_e = nullptr;   // ProxyBillingActivity.onActivityResult
static jobject g_backup_f = nullptr;   // MaxUnityAdManager.isRewardedAdReady
static jobject g_backup_g = nullptr;   // gpbl PurchaseUpdatedListenerProxy.onPurchasesUpdated
static jobject g_backup_h = nullptr;   // BillingClientImpl.launchBillingFlow
static jobject g_backup_i = nullptr;   // BillingClientImpl.acknowledgePurchase
static jobject g_backup_j = nullptr;   // BillingClientImpl.consumeAsync

static JNIEnv *get_jni_env() {
    JNIEnv *env = nullptr;
    jint gret = g_jvm->GetEnv((void **)&env, JNI_VERSION_1_6);
    LOGI("get_jni_env: GetEnv=%d", gret);
    if (gret != JNI_OK) {
        jint aret = g_jvm->AttachCurrentThread(&env, nullptr);
        LOGI("get_jni_env: AttachCurrentThread=%d env=%p", aret, (void*)env);
        if (aret != 0 || !env) {
            LOGE("get_jni_env: AttachCurrentThread FAILED ret=%d", aret);
            return nullptr;
        }
    }
    return env;
}

static jclass load_app_class(JNIEnv *env, const char *name) {
    if (!g_app_context) { LOGE("load_app_class: no app context"); return nullptr; }
    jclass ctx_cls = env->GetObjectClass(g_app_context);
    jmethodID gcl = env->GetMethodID(ctx_cls, "getClassLoader", "()Ljava/lang/ClassLoader;");
    jobject loader = env->CallObjectMethod(g_app_context, gcl);
    if (!loader) { env->ExceptionClear(); LOGE("load_app_class: no loader"); return nullptr; }
    jclass lc = env->FindClass("java/lang/ClassLoader");
    jmethodID load = env->GetMethodID(lc, "loadClass", "(Ljava/lang/String;)Ljava/lang/Class;");
    jstring jn = env->NewStringUTF(name);
    jclass cls = (jclass)env->CallObjectMethod(loader, load, jn);
    if (env->ExceptionCheck() || !cls) {
        env->ExceptionClear();
        LOGE("load_app_class: %s NOT found", name);
        env->DeleteLocalRef(jn);
        return nullptr;
    }
    jclass g = (jclass)env->NewGlobalRef(cls);
    env->DeleteLocalRef(cls);
    env->DeleteLocalRef(jn);
    env->DeleteLocalRef(loader);
    env->DeleteLocalRef(lc);
    LOGI("load_app_class: %s ok", name);
    return g;
}

static jstring make_fake_json_string(JNIEnv *env) {
    jlong now = 0;
    if (g_cls_system && g_mid_system_time)
        now = env->CallStaticLongMethod(g_cls_system, g_mid_system_time);
    const char *pid = "com.lavalabs.dungeonrush.gems_100";
    bool release = false;
    if (g_product_id) {
        const char *c = env->GetStringUTFChars(g_product_id, nullptr);
        if (c) { pid = c; release = true; }
    }
    char json[1024];
    snprintf(json, sizeof(json),
        "{\"orderId\":\"MOD.%lld\",\"packageName\":\"com.lavalabs.dungeonrush\","
        "\"productIds\":[\"%s\"],\"productId\":\"%s\",\"purchaseToken\":\"mod_token_%lld\","
        "\"purchaseTime\":%lld,\"purchaseState\":1,\"acknowledged\":false,\"autoRenewing\":false,\"quantity\":1,\"developerPayload\":\"\"}",
        (long long)now, pid, pid, (long long)now, (long long)now);
    if (release) env->ReleaseStringUTFChars(g_product_id, pid);
    jstring j = env->NewStringUTF(json);
    LOGI("fake purchase JSON built (productId=%s)", pid);
    return j;
}

static jobject make_fake_purchase(JNIEnv *env) {
    if (!g_cls_purchase || !g_mid_purchase_init) return nullptr;
    jstring jjson = make_fake_json_string(env);
    if (!jjson) return nullptr;
    // Use non-empty base64 signature to pass Unity validator (empty sig rejected)
    jstring jsig = env->NewStringUTF("MEUCIQDfak3SignatureForModBypassValidBase64Encoded1234567890==");
    jobject p = env->NewObject(g_cls_purchase, g_mid_purchase_init, jjson, jsig);
    if (env->ExceptionCheck()) { env->ExceptionClear(); p = nullptr; }
    env->DeleteLocalRef(jjson);
    env->DeleteLocalRef(jsig);
    if (p) LOGI("fake Purchase created");
    return p;
}

static jclass g_mc_class = nullptr;
static jmethodID g_mid_invoke = nullptr;

static void cache_reflection() {
    if (g_mc_class) return;
    JNIEnv *env = get_jni_env();
    if (!env) return;
    g_mc_class = (jclass)env->NewGlobalRef(env->FindClass("java/lang/reflect/Method"));
    g_mid_invoke = env->GetMethodID(g_mc_class, "invoke", "(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;");
}

static jobject call_backup(JNIEnv *env, jobject backup, jobject original_this, jobject *args, int argc) {
    if (!backup) { LOGI("call_backup: no backup, skipping"); return nullptr; }
    cache_reflection();
    if (!g_mid_invoke) { LOGE("call_backup: invoke mid not cached"); return nullptr; }
    jclass obj_cls = env->FindClass("java/lang/Object");
    jobjectArray jargs = env->NewObjectArray(argc, obj_cls, nullptr);
    env->DeleteLocalRef(obj_cls);
    for (int i = 0; i < argc; i++) env->SetObjectArrayElement(jargs, i, args[i]);
    jobject ret = env->CallObjectMethod(backup, g_mid_invoke, original_this, jargs);
    if (env->ExceptionCheck()) {
        env->ExceptionClear();
        ret = nullptr;
    }
    env->DeleteLocalRef(jargs);
    return ret;
}

static void send_ad_event(JNIEnv *env, const char *name, jobject adUnitId, jobject placement, const char *rewardLabel) {
    if (!g_cls_jsonobject || !g_mid_jsonobj_init || !g_mid_jsonobj_put || !g_cls_max || !g_mid_forward) return;
    jobject o = env->NewObject(g_cls_jsonobject, g_mid_jsonobj_init);
    if (!o) { env->ExceptionClear(); return; }
    jstring k, v;
    k = env->NewStringUTF("adUnitId");
    env->CallObjectMethod(o, g_mid_jsonobj_put, k, adUnitId);
    env->DeleteLocalRef(k);
    k = env->NewStringUTF("adFormat");
    v = env->NewStringUTF("REWARDED");
    env->CallObjectMethod(o, g_mid_jsonobj_put, k, v);
    env->DeleteLocalRef(k);
    env->DeleteLocalRef(v);
    k = env->NewStringUTF("networkName");
    v = env->NewStringUTF("ModBypass");
    env->CallObjectMethod(o, g_mid_jsonobj_put, k, v);
    env->DeleteLocalRef(k);
    env->DeleteLocalRef(v);
    if (placement) {
        k = env->NewStringUTF("networkPlacement");
        env->CallObjectMethod(o, g_mid_jsonobj_put, k, placement);
        env->DeleteLocalRef(k);
    }
    if (rewardLabel) {
        k = env->NewStringUTF("reward");
        jobject ro = env->NewObject(g_cls_jsonobject, g_mid_jsonobj_init);
        jstring lk = env->NewStringUTF("label");
        jstring lv = env->NewStringUTF(rewardLabel);
        env->CallObjectMethod(ro, g_mid_jsonobj_put, lk, lv);
        jstring ak = env->NewStringUTF("amount");
        jstring av = env->NewStringUTF("1");
        env->CallObjectMethod(ro, g_mid_jsonobj_put, ak, av);
        env->CallObjectMethod(o, g_mid_jsonobj_put, k, ro);
        env->DeleteLocalRef(lk); env->DeleteLocalRef(lv);
        env->DeleteLocalRef(ak); env->DeleteLocalRef(av);
        env->DeleteLocalRef(k);  env->DeleteLocalRef(ro);
    }
    k = env->NewStringUTF("name");
    v = env->NewStringUTF(name);
    env->CallObjectMethod(o, g_mid_jsonobj_put, k, v);
    env->DeleteLocalRef(k);
    env->DeleteLocalRef(v);
    env->CallStaticVoidMethod(g_cls_max, g_mid_forward, o);
    if (env->ExceptionCheck()) env->ExceptionClear();
    env->DeleteLocalRef(o);
}

static jobject make_success_billing_result(JNIEnv *env) {
    if (!env || !g_cls_billingresult || !g_mid_billing_newbuilder || !g_mid_builder_setResponseCode || !g_mid_builder_build) {
        LOGE("make_success_billing_result: missing billing builder classes/methods");
        return nullptr;
    }
    jobject builder = env->CallStaticObjectMethod(g_cls_billingresult, g_mid_billing_newbuilder);
    if (!builder || env->ExceptionCheck()) { env->ExceptionClear(); LOGE("make_success_billing_result: newBuilder failed"); return nullptr; }
    // setResponseCode(0) = OK
    jobject tmp = env->CallObjectMethod(builder, g_mid_builder_setResponseCode, 0);
    if (env->ExceptionCheck()) { env->ExceptionClear(); env->DeleteLocalRef(builder); LOGE("make_success_billing_result: setResponseCode failed"); return nullptr; }
    // builder may return new instance, update reference
    if (tmp && tmp != builder) { env->DeleteLocalRef(builder); builder = tmp; } else if (tmp) { /* same */ }
    // optional setDebugMessage("")
    if (g_mid_builder_setDebugMessage) {
        jstring dbg = env->NewStringUTF("");
        jobject tmp2 = env->CallObjectMethod(builder, g_mid_builder_setDebugMessage, dbg);
        if (!env->ExceptionCheck() && tmp2) {
            if (tmp2 != builder) { env->DeleteLocalRef(builder); builder = tmp2; }
        } else { env->ExceptionClear(); }
        env->DeleteLocalRef(dbg);
    }
    jobject result = env->CallObjectMethod(builder, g_mid_builder_build);
    if (env->ExceptionCheck()) { env->ExceptionClear(); result = nullptr; LOGE("make_success_billing_result: build failed"); }
    env->DeleteLocalRef(builder);
    if (result) LOGI("make_success_billing_result: OK BillingResult created %p", result);
    return result;
}

extern "C" JNIEXPORT jobject JNICALL
Java_com_drmod_HookerCallback_nativeDispatch(JNIEnv *env, jclass cls,
                                               jint hookId, jobject self_obj, jobjectArray args) {
    if (env && env->ExceptionCheck()) env->ExceptionClear();
    LOGI("nativeDispatch: === HOOK ENTRY hookId=%d caller=%p ===", hookId, __builtin_return_address(0));
    int argc = args ? env->GetArrayLength(args) : 0;
    if (env && env->ExceptionCheck()) env->ExceptionClear();
    LOGI("nativeDispatch: argc=%d", argc);
    // Extensive next-step prediction for store debugging
    const char* nextStep = "unknown";
    switch(hookId) {
        case 0: nextStep = "onQueryPurchasesResponse (gpbl) -> inject fake Purchase -> call backup -> game grants via StoreListenerProxy (check inventory next)"; break;
        case 3: nextStep = "queryProductDetailsAsync -> capture productId -> create fake ProductDetails -> call onProductDetailsResponse -> NEXT is launchBillingFlow (hookId 8)"; break;
        case 8: nextStep = "launchBillingFlow -> bypass Play -> create fake Purchase -> call onPurchasesUpdated ($Proxy) -> NEXT is my_google_pu -> my_proc_purchase -> my_dnf -> grant"; break;
        case 7: nextStep = "onPurchasesUpdated (gpbl) -> inject fake Purchase -> call backup -> next is queryPurchases or grant"; break;
        case 5: nextStep = "showRewardedAd -> send 3 fake ad events -> reward"; break;
        case 6: nextStep = "isRewardedAdReady -> force true"; break;
        case 4: nextStep = "ProxyBillingActivity.onActivityResult -> set RESPONSE_CODE 0 + INAPP lists"; break;
        case 9: nextStep = "acknowledgePurchase -> fake success"; break;
        case 10: nextStep = "consumeAsync -> fake success"; break;
    }
    LOGI("nativeDispatch: hookId=%d NEXT EXPECTED: %s", hookId, nextStep);

    jobject original_this = (argc > 0) ? env->GetObjectArrayElement(args, 0) : nullptr;

    switch (hookId) {
    case 0: { // gpbl PurchasesResponseListenerProxy.onQueryPurchasesResponse(Object result, List) — inject fake + forward
        LOGI("STORE_STEP: hookId=0 entry called by %p original_this=%p -> will inject fake", __builtin_return_address(0), original_this);
        jobject billingResult = argc > 1 ? env->GetObjectArrayElement(args, 1) : nullptr;
        jobject list = argc > 2 ? env->GetObjectArrayElement(args, 2) : nullptr;
        if (env && list) {
            // Forza il BillingResult a OK (responseCode=0) così il gioco non short-circuita sull'errore.
            // Prefer new BillingResult builder (8.3.0+), fallback to deprecated zzdc field if present.
            jobject br = make_success_billing_result(env);
            if (br) {
                if (billingResult) env->DeleteLocalRef(billingResult);
                billingResult = br;
                LOGI("STORE_STEP: onQueryPurchasesResponse BillingResult replaced with builder OK");
            } else if (billingResult && g_cls_zzcj && g_fid_zzcj_zzl) {
                jobject br2 = env->GetStaticObjectField(g_cls_zzcj, g_fid_zzcj_zzl);
                if (br2) {
                    env->DeleteLocalRef(billingResult);
                    billingResult = br2;
                    LOGI("onQueryPurchasesResponse: BillingResult replaced via deprecated zzdc");
                } else {
                    env->DeleteLocalRef(br2);
                    LOGI("onQueryPurchasesResponse: deprecated zzdc field fetch returned null");
                }
                env->ExceptionClear();
            }
            // Inietta fake con fallback per lista immutabile
            if (g_cls_purchase && g_mid_list_add && list) {
                jobject fake = make_fake_purchase(env);
                if (fake) {
                    env->CallBooleanMethod(list, g_mid_list_add, fake);
                    if (env->ExceptionCheck()) {
                        env->ExceptionClear();
                        LOGI("onQueryPurchasesResponse: list.add threw (immutable), creating mutable copy");
                        jobject copy = nullptr;
                        if (g_cls_arraylist && g_mid_arraylist_init) {
                            jclass alCls = env->FindClass("java/util/ArrayList");
                            jmethodID alInitCol = alCls ? env->GetMethodID(alCls, "<init>", "(Ljava/util/Collection;)V") : nullptr;
                            if (alInitCol) {
                                copy = env->NewObject(alCls, alInitCol, list);
                                if (env->ExceptionCheck()) { env->ExceptionClear(); copy = nullptr; }
                            }
                            if (alCls) env->DeleteLocalRef(alCls);
                            if (!copy) {
                                copy = env->NewObject(g_cls_arraylist, g_mid_arraylist_init);
                                if (copy && !env->ExceptionCheck()) {
                                    jmethodID addAll = env->GetMethodID(env->GetObjectClass(copy), "addAll", "(Ljava/util/Collection;)Z");
                                    if (addAll) { env->CallBooleanMethod(copy, addAll, list); if (env->ExceptionCheck()) env->ExceptionClear(); }
                                } else env->ExceptionClear();
                            }
                        }
                        if (copy) {
                            env->CallBooleanMethod(copy, g_mid_list_add, fake);
                            if (!env->ExceptionCheck()) {
                                env->DeleteLocalRef(list);
                                list = copy;
                                LOGI("onQueryPurchasesResponse(gpbl proxy): fake injected via mutable copy");
                            } else {
                                env->ExceptionClear();
                                env->DeleteLocalRef(copy);
                                LOGI("onQueryPurchasesResponse: mutable copy add also failed");
                            }
                        }
                    } else {
                        LOGI("onQueryPurchasesResponse(gpbl proxy): fake purchase injected");
                    }
                    env->DeleteLocalRef(fake);
                }
            }
            env->ExceptionClear();
        }
        // backup expects 2 params (BillingResult, List) - original_this is receiver
        jobject bargs[] = { billingResult, list };
        call_backup(env, g_backup_a, original_this, bargs, 2);
        env->DeleteLocalRef(billingResult);
        env->DeleteLocalRef(list);
        if (original_this) env->DeleteLocalRef(original_this);
        return nullptr;
    }
    case 1: { // zzbm.onQueryPurchasesResponse(BillingResult, List) — inject fake + forward
        jobject billingResult = argc > 1 ? env->GetObjectArrayElement(args, 1) : nullptr;
        jobject list = argc > 2 ? env->GetObjectArrayElement(args, 2) : nullptr;
        if (env && g_cls_purchase && list) {
            jobject fake = make_fake_purchase(env);
            if (fake && g_mid_list_add) {
                env->CallBooleanMethod(list, g_mid_list_add, fake);
                if (env->ExceptionCheck()) {
                    env->ExceptionClear();
                    jclass alCls = env->FindClass("java/util/ArrayList");
                    jmethodID alInitCol = alCls ? env->GetMethodID(alCls, "<init>", "(Ljava/util/Collection;)V") : nullptr;
                    jobject copy = alInitCol ? env->NewObject(alCls, alInitCol, list) : nullptr;
                    if (env->ExceptionCheck() || !copy) { env->ExceptionClear(); copy = env->NewObject(g_cls_arraylist, g_mid_arraylist_init); if (copy && !env->ExceptionCheck()) { jmethodID addAll = env->GetMethodID(env->GetObjectClass(copy), "addAll", "(Ljava/util/Collection;)Z"); if (addAll) env->CallBooleanMethod(copy, addAll, list); env->ExceptionClear(); } else env->ExceptionClear(); }
                    if (alCls) env->DeleteLocalRef(alCls);
                    if (copy) { env->CallBooleanMethod(copy, g_mid_list_add, fake); if (!env->ExceptionCheck()) { env->DeleteLocalRef(list); list = copy; } else { env->ExceptionClear(); env->DeleteLocalRef(copy); } }
                }
                env->DeleteLocalRef(fake);
                LOGI("onQueryPurchasesResponse: fake injected");
            }
            env->ExceptionClear();
        }
        jobject bargs[] = { billingResult, list };
        call_backup(env, g_backup_b, original_this, bargs, 2);
        env->DeleteLocalRef(billingResult);
        env->DeleteLocalRef(list);
        if (original_this) env->DeleteLocalRef(original_this);
        return nullptr;
    }
    case 2: { // BillingClientImpl.zzaI(String, PurchasesResponseListener) — direct delivery (deprecated path)
        if (env && g_cls_arraylist && g_mid_arraylist_init) {
            jobject br = make_success_billing_result(env);
            if (!br && g_cls_zzcj && g_fid_zzcj_zzl) {
                br = env->GetStaticObjectField(g_cls_zzcj, g_fid_zzcj_zzl);
                env->ExceptionClear();
            }
            jobject fake = make_fake_purchase(env);
            jobject list = env->NewObject(g_cls_arraylist, g_mid_arraylist_init);
            if (fake && list && g_mid_list_add) {
                env->CallBooleanMethod(list, g_mid_list_add, fake);
                jobject listener = argc > 2 ? env->GetObjectArrayElement(args, 2) : nullptr;
                if (listener) {
                    jclass lc = env->GetObjectClass(listener);
                    jmethodID m = env->GetMethodID(lc, "onQueryPurchasesResponse",
                        "(Lcom/android/billingclient/api/BillingResult;Ljava/util/List;)V");
                    if (m) {
                        env->CallVoidMethod(listener, m, br, list);
                        LOGI("zzaI: fake delivered to listener");
                    }
                    env->DeleteLocalRef(listener);
                }
            }
            env->ExceptionClear();
            env->DeleteLocalRef(br);
            env->DeleteLocalRef(fake);
            env->DeleteLocalRef(list);
        }
        if (original_this) env->DeleteLocalRef(original_this);
        return nullptr; // skip original
    }
    case 3: { // BillingClientImpl.queryProductDetailsAsync — capture productId + forward
        if (env && env->ExceptionCheck()) env->ExceptionClear();
        jobject params = argc > 1 ? env->GetObjectArrayElement(args, 1) : nullptr;
        jobject listener = argc > 2 ? env->GetObjectArrayElement(args, 2) : nullptr;
        LOGI("STORE_STEP: hookId=3 entry called by %p, original_this=%p BillingClient %p, params=%p listener=%p -> will capture productId -> create ProductDetails -> QueryResult -> BillingResult -> call onProductDetailsResponse -> next is launchBillingFlow:8", __builtin_return_address(0), original_this, original_this, params, listener);
        // Fake ProductDetails path to avoid Play roundtrip: create BillingResult OK + ProductDetails with captured productId
        bool needFake = false;
        const char *captured = nullptr;
        char capturedBuf[256] = {0};
        char allIdsBuf[1024] = {0};
        if (env && params) {
            jclass pc = env->GetObjectClass(params);
            // Log params class name
            {
                jclass classCls = env->FindClass("java/lang/Class");
                if (classCls) {
                    jmethodID getNameMid = env->GetMethodID(classCls, "getName", "()Ljava/lang/String;");
                    if (getNameMid && pc) {
                        jstring cname = (jstring)env->CallObjectMethod(pc, getNameMid);
                        if (cname && !env->ExceptionCheck()) {
                            const char *cn = env->GetStringUTFChars(cname, nullptr);
                            LOGI("queryProductDetailsAsync params class=%s", cn ? cn : "null");
                            if (cn) env->ReleaseStringUTFChars(cname, cn);
                            env->DeleteLocalRef(cname);
                        } else env->ExceptionClear();
                    }
                    env->DeleteLocalRef(classCls);
                }
                env->ExceptionClear();
            }
            jobject productList = nullptr;
            jclass listCls = nullptr;
            // Try candidate method names for List extraction (covers 8.3.1 obfuscations)
            const char* listMethodNames[] = {"getProductList", "zza", "zzb", "zzc", "zzd", "zze", "getProducts", nullptr};
            const char* listSigs[] = {"()Ljava/util/List;", "()Ljava/lang/Object;", "()Ljava/util/ArrayList;", nullptr};
            for (int ni = 0; listMethodNames[ni] && !productList; ni++) {
                for (int si = 0; listSigs[si] && !productList; si++) {
                    jmethodID mid = env->GetMethodID(pc, listMethodNames[ni], listSigs[si]);
                    if (env->ExceptionCheck()) env->ExceptionClear();
                    if (!mid) continue;
                    LOGI("queryProductDetailsAsync: trying %s %s -> %p", listMethodNames[ni], listSigs[si], (void*)mid);
                    jobject res = env->CallObjectMethod(params, mid);
                    if (env->ExceptionCheck()) { env->ExceptionDescribe(); env->ExceptionClear(); res = nullptr; LOGI("  call %s threw", listMethodNames[ni]); }
                    if (res) {
                        jclass rc = env->GetObjectClass(res);
                        jclass cls2 = env->FindClass("java/lang/Class");
                        jmethodID gn2 = cls2 ? env->GetMethodID(cls2, "getName", "()Ljava/lang/String;") : nullptr;
                        if (gn2 && rc) {
                            jstring rn = (jstring)env->CallObjectMethod(rc, gn2);
                            if (rn && !env->ExceptionCheck()) {
                                const char *rnc = env->GetStringUTFChars(rn, nullptr);
                                LOGI("  %s returned %s (%p)", listMethodNames[ni], rnc ? rnc : "null", res);
                                if (rnc) env->ReleaseStringUTFChars(rn, rnc);
                                env->DeleteLocalRef(rn);
                            } else env->ExceptionClear();
                        }
                        if (cls2) env->DeleteLocalRef(cls2);
                        // check if List
                        jclass lstCheck = env->FindClass("java/util/List");
                        if (lstCheck && env->IsInstanceOf(res, lstCheck)) {
                            productList = res; // keep ref, don't delete yet
                            listCls = env->GetObjectClass(productList);
                            LOGI("  -> accepted as List via %s", listMethodNames[ni]);
                        } else {
                            LOGI("  -> not a List, discarding");
                            env->DeleteLocalRef(res);
                        }
                        if (lstCheck) env->DeleteLocalRef(lstCheck);
                        if (rc) env->DeleteLocalRef(rc);
                    }
                }
            }
            // Brute force via reflection if still null: scan declared methods (including private zza/zzb) and try any no-arg that returns list-like
            if (!productList) {
                LOGI("queryProductDetailsAsync: brute force reflection scan (declared)");
                jclass classCls = env->FindClass("java/lang/Class");
                if (env->ExceptionCheck()) env->ExceptionClear();
                jmethodID getDeclaredMethodsMid = classCls ? env->GetMethodID(classCls, "getDeclaredMethods", "()[Ljava/lang/reflect/Method;") : nullptr;
                if (env->ExceptionCheck()) env->ExceptionClear();
                if (getDeclaredMethodsMid && pc) {
                    jobjectArray methods = (jobjectArray)env->CallObjectMethod(pc, getDeclaredMethodsMid);
                    if (env->ExceptionCheck()) { env->ExceptionClear(); methods = nullptr; }
                    if (methods) {
                        int mlen = env->GetArrayLength(methods);
                        LOGI("  reflection declared: %d methods on params class", mlen);
                        jclass methodCls = env->FindClass("java/lang/reflect/Method");
                        if (env->ExceptionCheck()) env->ExceptionClear();
                        jmethodID getNameMid = methodCls ? env->GetMethodID(methodCls, "getName", "()Ljava/lang/String;") : nullptr;
                        jmethodID getRetMid = methodCls ? env->GetMethodID(methodCls, "getReturnType", "()Ljava/lang/Class;") : nullptr;
                        jmethodID invokeMid = methodCls ? env->GetMethodID(methodCls, "invoke", "(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;") : nullptr;
                        jmethodID setAccMid = methodCls ? env->GetMethodID(methodCls, "setAccessible", "(Z)V") : nullptr;
                        jmethodID getParamCountMid = methodCls ? env->GetMethodID(methodCls, "getParameterCount", "()I") : nullptr;
                        for (int i = 0; i < mlen && !productList; i++) {
                            jobject m = env->GetObjectArrayElement(methods, i);
                            if (!m) continue;
                            // Only no-arg methods
                            int paramCount = -1;
                            if (getParamCountMid) {
                                paramCount = env->CallIntMethod(m, getParamCountMid);
                                if (env->ExceptionCheck()) { env->ExceptionClear(); paramCount = -1; }
                                if (paramCount != 0) { env->DeleteLocalRef(m); continue; }
                            }
                            jstring mnameJ = getNameMid ? (jstring)env->CallObjectMethod(m, getNameMid) : nullptr;
                            jobject retCls = getRetMid ? env->CallObjectMethod(m, getRetMid) : nullptr;
                            const char *mnameC = nullptr;
                            const char *retNameC = nullptr;
                            jstring retNameJ = nullptr;
                            if (mnameJ) mnameC = env->GetStringUTFChars(mnameJ, nullptr);
                            if (retCls && classCls) {
                                jmethodID gn = env->GetMethodID(classCls, "getName", "()Ljava/lang/String;");
                                if (env->ExceptionCheck()) env->ExceptionClear();
                                retNameJ = gn ? (jstring)env->CallObjectMethod(retCls, gn) : nullptr;
                                if (retNameJ && !env->ExceptionCheck()) retNameC = env->GetStringUTFChars(retNameJ, nullptr);
                                else env->ExceptionClear();
                            }
                            // Try invoke regardless of return type (internal wrapper may not be List)
                            if (invokeMid) {
                                if (setAccMid) { env->CallVoidMethod(m, setAccMid, JNI_TRUE); if (env->ExceptionCheck()) env->ExceptionClear(); }
                                jobject emptyArgs = env->NewObjectArray(0, env->FindClass("java/lang/Object"), nullptr);
                                if (env->ExceptionCheck()) env->ExceptionClear();
                                jobject res = env->CallObjectMethod(m, invokeMid, params, emptyArgs);
                                if (env->ExceptionCheck()) { env->ExceptionClear(); res = nullptr; }
                                if (res) {
                                    // Check if res is List or list-like (has size/get)
                                    bool handled = false;
                                    jclass lstCheck = env->FindClass("java/util/List");
                                    if (env->ExceptionCheck()) env->ExceptionClear();
                                    if (lstCheck && env->IsInstanceOf(res, lstCheck)) {
                                        jclass rc = env->GetObjectClass(res);
                                        jmethodID szMid = env->GetMethodID(rc, "size", "()I");
                                        if (env->ExceptionCheck()) env->ExceptionClear();
                                        int sz = -1;
                                        if (szMid) { sz = env->CallIntMethod(res, szMid); if (env->ExceptionCheck()) { env->ExceptionClear(); sz = -1; } }
                                        LOGI("  reflection invoke %s -> List size=%d (%p) ret=%s", mnameC?mnameC:"?", sz, res, retNameC?retNameC:"?");
                                        if (sz >= 0) { // accept even 0 to know method works, but need >0 to capture
                                            if (sz > 0) { productList = res; listCls = rc; handled = true; }
                                            else { env->DeleteLocalRef(res); env->DeleteLocalRef(rc); handled = true; }
                                        } else {
                                            // not List-like via size, try getDeclared
                                        }
                                        if (handled && productList) { /* keep */ } else if (!handled) { env->DeleteLocalRef(res); if (sz>=0) env->DeleteLocalRef(rc); }
                                        if (lstCheck) env->DeleteLocalRef(lstCheck);
                                        if (!handled) {
                                            // try list-like via size/get on any object
                                            jclass rc2 = env->GetObjectClass(res);
                                            jmethodID sz2 = env->GetMethodID(rc2, "size", "()I");
                                            if (env->ExceptionCheck()) env->ExceptionClear();
                                            jmethodID get2 = env->GetMethodID(rc2, "get", "(I)Ljava/lang/Object;");
                                            if (env->ExceptionCheck()) env->ExceptionClear();
                                            if (sz2 && get2) {
                                                int szTry = env->CallIntMethod(res, sz2);
                                                if (!env->ExceptionCheck() && szTry > 0) {
                                                    LOGI("  reflection invoke %s -> list-like size=%d via size/get", mnameC?mnameC:"?", szTry);
                                                    productList = res; listCls = rc2; handled = true;
                                                } else { env->ExceptionClear(); env->DeleteLocalRef(rc2); env->DeleteLocalRef(res); }
                                            } else { if (rc2) env->DeleteLocalRef(rc2); env->DeleteLocalRef(res); }
                                        } else if (!productList && lstCheck) {
                                            // already handled list case but size 0, need to try next method
                                        }
                                    } else {
                                        // Not a List, try size/get heuristic
                                        jclass rc2 = env->GetObjectClass(res);
                                        jmethodID sz2 = env->GetMethodID(rc2, "size", "()I");
                                        if (env->ExceptionCheck()) env->ExceptionClear();
                                        jmethodID get2 = env->GetMethodID(rc2, "get", "(I)Ljava/lang/Object;");
                                        if (env->ExceptionCheck()) env->ExceptionClear();
                                        if (sz2 && get2) {
                                            int szTry = env->CallIntMethod(res, sz2);
                                            if (env->ExceptionCheck()) { env->ExceptionClear(); szTry = -1; }
                                            if (szTry > 0) {
                                                jobject first = env->CallObjectMethod(res, get2, 0);
                                                if (!env->ExceptionCheck() && first) {
                                                    LOGI("  reflection invoke %s -> list-like wrapper size=%d first=%p ret=%s", mnameC?mnameC:"?", szTry, first, retNameC?retNameC:"?");
                                                    env->DeleteLocalRef(first);
                                                    productList = res; listCls = rc2; // keep wrapper as list holder
                                                } else { env->ExceptionClear(); env->DeleteLocalRef(rc2); env->DeleteLocalRef(res); }
                                            } else { env->DeleteLocalRef(rc2); env->DeleteLocalRef(res); }
                                        } else { if (rc2) env->DeleteLocalRef(rc2); env->DeleteLocalRef(res); }
                                        if (lstCheck) env->DeleteLocalRef(lstCheck);
                                    }
                                }
                                env->DeleteLocalRef(emptyArgs);
                            }
                            if (mnameC) env->ReleaseStringUTFChars(mnameJ, mnameC);
                            if (retNameC) env->ReleaseStringUTFChars(retNameJ, retNameC);
                            if (mnameJ) env->DeleteLocalRef(mnameJ);
                            if (retNameJ) env->DeleteLocalRef(retNameJ);
                            if (retCls) env->DeleteLocalRef(retCls);
                            env->DeleteLocalRef(m);
                        }
                        env->DeleteLocalRef(methods);
                        if (methodCls) env->DeleteLocalRef(methodCls);
                    } else env->ExceptionClear();
                    if (classCls) env->DeleteLocalRef(classCls);
                }
            }
            if (productList && listCls) {
                jmethodID sizeMid = env->GetMethodID(listCls, "size", "()I");
                jmethodID getMid = env->GetMethodID(listCls, "get", "(I)Ljava/lang/Object;");
                if (!sizeMid || !getMid) {
                    // try interfaces: List.class
                    jclass lst = env->FindClass("java/util/List");
                    if (lst) {
                        if (!sizeMid) sizeMid = env->GetMethodID(lst, "size", "()I");
                        if (!getMid) getMid = env->GetMethodID(lst, "get", "(I)Ljava/lang/Object;");
                        env->DeleteLocalRef(lst);
                    }
                }
                int sz = sizeMid ? env->CallIntMethod(productList, sizeMid) : 0;
                if (env->ExceptionCheck()) { env->ExceptionClear(); sz = 0; }
                LOGI("queryProductDetailsAsync: productList size=%d", sz);
                const char* idCandidates[] = {"getProductId", "zza", "zzb", "zzc", "getProduct", "getId", "getProductIdString", nullptr};
                for (int idx = 0; idx < sz; idx++) {
                    jobject prod = getMid ? env->CallObjectMethod(productList, getMid, idx) : nullptr;
                    if (env->ExceptionCheck()) { env->ExceptionClear(); prod = nullptr; }
                    if (!prod) { LOGI("  [%d] null", idx); continue; }
                    jclass pc2 = env->GetObjectClass(prod);
                    // log prod class
                    {
                        jclass cc = env->FindClass("java/lang/Class");
                        jmethodID gn = cc ? env->GetMethodID(cc, "getName", "()Ljava/lang/String;") : nullptr;
                        jstring pn = gn ? (jstring)env->CallObjectMethod(pc2, gn) : nullptr;
                        if (pn && !env->ExceptionCheck()) {
                            const char *pnc = env->GetStringUTFChars(pn, nullptr);
                            LOGI("  [%d] prod class=%s", idx, pnc?pnc:"null");
                            if (pnc) env->ReleaseStringUTFChars(pn, pnc);
                            env->DeleteLocalRef(pn);
                        } else env->ExceptionClear();
                        if (cc) env->DeleteLocalRef(cc);
                    }
                    jstring id = nullptr;
                    const char* foundVia = nullptr;
                    for (int ci = 0; idCandidates[ci] && !id; ci++) {
                        jmethodID mid = env->GetMethodID(pc2, idCandidates[ci], "()Ljava/lang/String;");
                        if (env->ExceptionCheck()) env->ExceptionClear();
                        if (!mid) continue;
                        jstring res = (jstring)env->CallObjectMethod(prod, mid);
                        if (env->ExceptionCheck()) { env->ExceptionClear(); res = nullptr; continue; }
                        if (res) { id = res; foundVia = idCandidates[ci]; LOGI("  [%d] get via %s ok", idx, foundVia); }
                    }
                    // field fallback
                    if (!id) {
                        jclass cc = env->FindClass("java/lang/Class");
                        jmethodID gdm = cc ? env->GetMethodID(cc, "getDeclaredFields", "()[Ljava/lang/reflect/Field;") : nullptr;
                        if (gdm) {
                            jobjectArray fields = (jobjectArray)env->CallObjectMethod(pc2, gdm);
                            if (!env->ExceptionCheck() && fields) {
                                int flen = env->GetArrayLength(fields);
                                jclass fieldCls = env->FindClass("java/lang/reflect/Field");
                                jmethodID getNameMid = fieldCls ? env->GetMethodID(fieldCls, "getName", "()Ljava/lang/String;") : nullptr;
                                jmethodID getTypeMid = fieldCls ? env->GetMethodID(fieldCls, "getType", "()Ljava/lang/Class;") : nullptr;
                                jmethodID getMid2 = fieldCls ? env->GetMethodID(fieldCls, "get", "(Ljava/lang/Object;)Ljava/lang/Object;") : nullptr;
                                jmethodID setAccMid = fieldCls ? env->GetMethodID(fieldCls, "setAccessible", "(Z)V") : nullptr;
                                for (int fi = 0; fi < flen && !id; fi++) {
                                    jobject f = env->GetObjectArrayElement(fields, fi);
                                    if (!f) continue;
                                    jstring fnJ = getNameMid ? (jstring)env->CallObjectMethod(f, getNameMid) : nullptr;
                                    jobject ft = getTypeMid ? env->CallObjectMethod(f, getTypeMid) : nullptr;
                                    const char *fnC = fnJ ? env->GetStringUTFChars(fnJ, nullptr) : nullptr;
                                    const char *ftC = nullptr;
                                    jstring ftNameJ = nullptr;
                                    if (ft && cc) {
                                        jmethodID gn = env->GetMethodID(cc, "getName", "()Ljava/lang/String;");
                                        ftNameJ = gn ? (jstring)env->CallObjectMethod(ft, gn) : nullptr;
                                        if (ftNameJ && !env->ExceptionCheck()) ftC = env->GetStringUTFChars(ftNameJ, nullptr);
                                        else env->ExceptionClear();
                                    }
                                    bool isString = ftC && strstr(ftC, "java.lang.String");
                                    if (isString && setAccMid) {
                                        env->CallVoidMethod(f, setAccMid, JNI_TRUE);
                                        if (!env->ExceptionCheck()) {
                                            jobject v = getMid2 ? env->CallObjectMethod(f, getMid2, prod) : nullptr;
                                            if (!env->ExceptionCheck() && v) {
                                                id = (jstring)v; // keep
                                                LOGI("  [%d] field %s -> %p", idx, fnC?fnC:"?", id);
                                            } else env->ExceptionClear();
                                        } else env->ExceptionClear();
                                    }
                                    if (fnC) env->ReleaseStringUTFChars(fnJ, fnC);
                                    if (ftC) env->ReleaseStringUTFChars(ftNameJ, ftC);
                                    if (fnJ) env->DeleteLocalRef(fnJ);
                                    if (ftNameJ) env->DeleteLocalRef(ftNameJ);
                                    if (ft) env->DeleteLocalRef(ft);
                                    env->DeleteLocalRef(f);
                                }
                                env->DeleteLocalRef(fields);
                                if (fieldCls) env->DeleteLocalRef(fieldCls);
                            } else env->ExceptionClear();
                        }
                        if (cc) env->DeleteLocalRef(cc);
                    }
                    if (id) {
                        const char *c = env->GetStringUTFChars(id, nullptr);
                        LOGI("  [%d] productId='%s' via %s", idx, c?c:"null", foundVia?foundVia:"field");
                        if (c && strcmp(c, "inapp")!=0 && strcmp(c, "subs")!=0 && strlen(c) > 0 && strlen(c) < sizeof(capturedBuf)-1) {
                            if (strlen(allIdsBuf) + strlen(c) + 2 < sizeof(allIdsBuf)) {
                                if (strlen(allIdsBuf)) strncat(allIdsBuf, ",", sizeof(allIdsBuf)-strlen(allIdsBuf)-1);
                                strncat(allIdsBuf, c, sizeof(allIdsBuf)-strlen(allIdsBuf)-1);
                            }
                            if (!captured) {
                                strncpy(capturedBuf, c, sizeof(capturedBuf)-1);
                                captured = capturedBuf;
                                if (g_product_id) env->DeleteGlobalRef(g_product_id);
                                g_product_id = (jstring)env->NewGlobalRef(id);
                                LOGI("productId captured (first): %s (all=%s)", c, allIdsBuf);
                                needFake = true;
                            }
                        }
                        if (c) env->ReleaseStringUTFChars(id, c);
                        env->DeleteLocalRef(id);
                    } else {
                        LOGI("  [%d] productId NOT FOUND", idx);
                    }
                    env->DeleteLocalRef(pc2);
                    env->DeleteLocalRef(prod);
                }
                if (strlen(allIdsBuf)) LOGI("queryProductDetailsAsync: ALL productIds=%s (count=%d)", allIdsBuf, sz);
                else LOGI("queryProductDetailsAsync: NO valid productIds in list size=%d", sz);
                env->DeleteLocalRef(productList);
                env->DeleteLocalRef(listCls);
            } else {
                LOGE("queryProductDetailsAsync: cannot obtain productList (params class methods failed)");
                // log all methods for debug
                jclass cc = env->FindClass("java/lang/Class");
                jmethodID gms = cc ? env->GetMethodID(cc, "getMethods", "()[Ljava/lang/reflect/Method;") : nullptr;
                if (gms && pc) {
                    jobjectArray ms = (jobjectArray)env->CallObjectMethod(pc, gms);
                    if (!env->ExceptionCheck() && ms) {
                        int l = env->GetArrayLength(ms);
                        LOGI("  dumping %d methods for params class:", l);
                        jclass mc = env->FindClass("java/lang/reflect/Method");
                        jmethodID gnm = mc ? env->GetMethodID(mc, "getName", "()Ljava/lang/String;") : nullptr;
                        for (int i=0;i<l && i<30;i++) {
                            jobject m = env->GetObjectArrayElement(ms,i);
                            jstring n = gnm ? (jstring)env->CallObjectMethod(m, gnm) : nullptr;
                            const char *nc = n ? env->GetStringUTFChars(n,nullptr) : nullptr;
                            LOGI("    method[%d]=%s", i, nc?nc:"null");
                            if (nc) env->ReleaseStringUTFChars(n,nc);
                            if (n) env->DeleteLocalRef(n);
                            env->DeleteLocalRef(m);
                        }
                        env->DeleteLocalRef(ms);
                        if (mc) env->DeleteLocalRef(mc);
                    } else env->ExceptionClear();
                }
                if (cc) env->DeleteLocalRef(cc);
            }
            env->DeleteLocalRef(pc);
            env->ExceptionClear();
        }
        // Log what we are buying and forward to Play for REAL ProductDetails (no fake, no UI) - use fake JSON only in launchBillingFlow
        if (needFake) {
            LOGI("STORE_STEP: queryProductDetailsAsync captured productIds=%s (first=%s) -> forwarding to Play for REAL ProductDetails (no fake, no UI)", allIdsBuf, captured?captured:"null");
            LOGI("BUY_LOG: productId=%s (all=%s) - query real details from Play", captured?captured:"null", allIdsBuf);
        } else {
            LOGI("STORE_STEP: queryProductDetailsAsync no productIds captured (allIds=%s) -> forwarding to Play anyway for real details", allIdsBuf);
            if (strlen(allIdsBuf)) LOGI("BUY_LOG: will fetch real ProductDetails for %s", allIdsBuf);
        }
        if (captured) LOGI("BUY_LOG: will fetch real ProductDetails for %s", captured);
        // Forward to original to get REAL ProductDetails from Play (query has no UI)
        jobject bargs[] = { params, listener };
        call_backup(env, g_backup_d, original_this, bargs, 2);
        LOGI("STORE_STEP: queryProductDetailsAsync forwarded to Play, real onProductDetailsResponse will follow");
        env->DeleteLocalRef(params);
        env->DeleteLocalRef(listener);
        if (original_this) env->DeleteLocalRef(original_this);
        return nullptr;
    }
    case 4: { // ProxyBillingActivity.onActivityResult(int, int, Intent)
        jobject intent = argc > 3 ? env->GetObjectArrayElement(args, 3) : nullptr;
        if (env && intent && g_cls_intent && g_cls_arraylist && g_mid_arraylist_init) {
            if (g_mid_intent_putextra_int) {
                jstring k = env->NewStringUTF("RESPONSE_CODE");
                env->CallObjectMethod(intent, g_mid_intent_putextra_int, k, 0);
                env->DeleteLocalRef(k);
            }
            jstring json = make_fake_json_string(env);
            if (json) {
                jobject dataList = env->NewObject(g_cls_arraylist, g_mid_arraylist_init);
                env->CallBooleanMethod(dataList, g_mid_list_add, json);
                jobject sigList = env->NewObject(g_cls_arraylist, g_mid_arraylist_init);
                jstring sig = env->NewStringUTF("");
                env->CallBooleanMethod(sigList, g_mid_list_add, sig);
                jobject itemList = env->NewObject(g_cls_arraylist, g_mid_arraylist_init);
                if (g_product_id) env->CallBooleanMethod(itemList, g_mid_list_add, g_product_id);
                if (g_mid_intent_putsal) {
                    jstring k1 = env->NewStringUTF("INAPP_PURCHASE_DATA_LIST");
                    env->CallObjectMethod(intent, g_mid_intent_putsal, k1, dataList);
                    env->DeleteLocalRef(k1);
                    jstring k2 = env->NewStringUTF("INAPP_DATA_SIGNATURE_LIST");
                    env->CallObjectMethod(intent, g_mid_intent_putsal, k2, sigList);
                    env->DeleteLocalRef(k2);
                    jstring k3 = env->NewStringUTF("INAPP_PURCHASE_ITEM_LIST");
                    env->CallObjectMethod(intent, g_mid_intent_putsal, k3, itemList);
                    env->DeleteLocalRef(k3);
                }
                env->DeleteLocalRef(dataList);
                env->DeleteLocalRef(sigList);
                env->DeleteLocalRef(itemList);
                env->DeleteLocalRef(sig);
                env->DeleteLocalRef(json);
            }
            env->ExceptionClear();
            LOGI("onActivityResult: RESPONSE_CODE=0 + fake extras set");
        }
        jobject reqCode = argc > 1 ? env->GetObjectArrayElement(args, 1) : nullptr;
        jobject resCode = argc > 2 ? env->GetObjectArrayElement(args, 2) : nullptr;
        jobject bargs[] = { reqCode, resCode, intent };
        call_backup(env, g_backup_e, original_this, bargs, 3);
        if (reqCode) env->DeleteLocalRef(reqCode);
        if (resCode) env->DeleteLocalRef(resCode);
        env->DeleteLocalRef(intent);
        if (original_this) env->DeleteLocalRef(original_this);
        return nullptr;
    }
    case 5: { // MaxUnityAdManager.showRewardedAd(String, String, String) — fake events
        jobject adUnitId = argc > 1 ? env->GetObjectArrayElement(args, 1) : nullptr;
        jobject placement = argc > 2 ? env->GetObjectArrayElement(args, 2) : nullptr;
        g_last_ad_reward_ms = eyu_now_ms(); // v4.81: apre finestra B (bonus keys xMULT su eventuale incremento)
        send_ad_event(env, "OnRewardedAdDisplayedEvent", adUnitId, placement, nullptr);
        send_ad_event(env, "OnRewardedAdReceivedRewardEvent", adUnitId, placement, "reward");
        send_ad_event(env, "OnRewardedAdHiddenEvent", adUnitId, placement, nullptr);
        LOGI("showRewardedAd: 3 fake events sent (g_last_ad_reward_ms=%lld)", (long long)g_last_ad_reward_ms);
        env->DeleteLocalRef(adUnitId);
        env->DeleteLocalRef(placement);
        if (original_this) env->DeleteLocalRef(original_this);
        return nullptr; // skip original
    }
    case 6: { // MaxUnityAdManager.isRewardedAdReady(String) — force true
        if (env && g_cls_boolean && g_mid_boolean_valueof) {
            jlong now = 0;
            if (g_cls_system && g_mid_system_time) now = env->CallStaticLongMethod(g_cls_system, g_mid_system_time);
            static jlong lastRewardMs = 0;
            if (now - lastRewardMs > 500) {
                jobject adUnitId = argc > 1 ? env->GetObjectArrayElement(args, 1) : nullptr;
                if (adUnitId) {
                    g_last_ad_reward_ms = eyu_now_ms(); // v4.81: apre finestra B (bonus keys xMULT)
                    send_ad_event(env, "OnRewardedAdDisplayedEvent", adUnitId, nullptr, nullptr);
                    send_ad_event(env, "OnRewardedAdReceivedRewardEvent", adUnitId, nullptr, "reward");
                    send_ad_event(env, "OnRewardedAdHiddenEvent", adUnitId, nullptr, nullptr);
                    lastRewardMs = now;
                    env->DeleteLocalRef(adUnitId);
                }
            }
            jobject boxed = env->CallStaticObjectMethod(g_cls_boolean, g_mid_boolean_valueof, JNI_TRUE);
            env->ExceptionClear();
            return boxed;
        }
        // Fallback: rilancia l'originale (restituisce Boolean già boxed).
        jobject adUnitIdFb = argc > 1 ? env->GetObjectArrayElement(args, 1) : nullptr;
        jobject bargs[] = { adUnitIdFb };
        jobject ret = call_backup(env, g_backup_f, original_this, bargs, 1);
        if (adUnitIdFb) env->DeleteLocalRef(adUnitIdFb);
        if (original_this) env->DeleteLocalRef(original_this);
        return ret;
    }
    case 7: { // gpbl PurchaseUpdatedListenerProxy.onPurchasesUpdated(Object result, List) — purchase flow
        LOGI("STORE_STEP: hookId=7 entry called by %p original_this=%p -> will inject fake", __builtin_return_address(0), original_this);
        jobject billingResult = argc > 1 ? env->GetObjectArrayElement(args, 1) : nullptr;
        jobject list = argc > 2 ? env->GetObjectArrayElement(args, 2) : nullptr;
        if (env && list) {
            jobject br = make_success_billing_result(env);
            if (br) {
                if (billingResult) env->DeleteLocalRef(billingResult);
                billingResult = br;
                LOGI("STORE_STEP: onPurchasesUpdated BillingResult replaced with builder OK");
            }
            if (g_cls_purchase && g_mid_list_add && list) {
                jobject fake = make_fake_purchase(env);
                if (fake) {
                    env->CallBooleanMethod(list, g_mid_list_add, fake);
                    if (env->ExceptionCheck()) {
                        env->ExceptionClear();
                        LOGI("onPurchasesUpdated: list.add threw (immutable), creating mutable copy");
                        jobject copy = nullptr;
                        if (g_cls_arraylist && g_mid_arraylist_init) {
                            jclass alCls = env->FindClass("java/util/ArrayList");
                            jmethodID alInitCol = alCls ? env->GetMethodID(alCls, "<init>", "(Ljava/util/Collection;)V") : nullptr;
                            if (alInitCol) {
                                copy = env->NewObject(alCls, alInitCol, list);
                                if (env->ExceptionCheck()) { env->ExceptionClear(); copy = nullptr; }
                            }
                            if (alCls) env->DeleteLocalRef(alCls);
                            if (!copy) {
                                copy = env->NewObject(g_cls_arraylist, g_mid_arraylist_init);
                                if (copy && !env->ExceptionCheck()) {
                                    jmethodID addAll = env->GetMethodID(env->GetObjectClass(copy), "addAll", "(Ljava/util/Collection;)Z");
                                    if (addAll) { env->CallBooleanMethod(copy, addAll, list); if (env->ExceptionCheck()) env->ExceptionClear(); }
                                } else env->ExceptionClear();
                            }
                        }
                        if (copy) {
                            env->CallBooleanMethod(copy, g_mid_list_add, fake);
                            if (!env->ExceptionCheck()) {
                                env->DeleteLocalRef(list);
                                list = copy;
                                LOGI("onPurchasesUpdated: fake injected via mutable copy");
                            } else { env->ExceptionClear(); env->DeleteLocalRef(copy); }
                        }
                    } else {
                        LOGI("STORE_STEP: onPurchasesUpdated fake purchase injected");
                    }
                    env->DeleteLocalRef(fake);
                }
            }
            env->ExceptionClear();
        }
        jobject bargs[] = { billingResult, list };
        call_backup(env, g_backup_g, original_this, bargs, 2);
        env->DeleteLocalRef(billingResult);
        env->DeleteLocalRef(list);
        if (original_this) env->DeleteLocalRef(original_this);
        return nullptr;
    }
    case 8: { {
        if (env && env->ExceptionCheck()) env->ExceptionClear();
        LOGI("STORE_STEP: hookId=8 entry called by %p original_this=%p -> will bypass Play", __builtin_return_address(0), original_this);
        jobject activity = argc > 1 ? env->GetObjectArrayElement(args, 1) : nullptr;
        jobject params = argc > 2 ? env->GetObjectArrayElement(args, 2) : nullptr;
        LOGI("STORE_STEP: launchBillingFlow intercepted activity=%p params=%p", activity, params);
        // Find PurchasesUpdatedListener stored in BillingClientImpl.zzf (zzab)
        jobject fakeLocal = make_fake_purchase(env);
        jobject fake = fakeLocal ? env->NewGlobalRef(fakeLocal) : nullptr;
        if (fakeLocal) env->DeleteLocalRef(fakeLocal);
        jobject brLocal = make_success_billing_result(env);
        jobject br = brLocal ? env->NewGlobalRef(brLocal) : nullptr;
        if (brLocal) env->DeleteLocalRef(brLocal);
        if (g_product_id) {
            const char* _c = env->GetStringUTFChars(g_product_id, nullptr);
            LOGI("BUY_LOG: launchBillingFlow purchasing %s (fake JSON, ProductDetails real from Play)", _c?_c:"null");
            if (_c) env->ReleaseStringUTFChars(g_product_id, _c);
        } else LOGI("BUY_LOG: launchBillingFlow purchasing fallback lootio.gem (g_product_id null)");
        // Also try to extract productId directly from BillingFlowParams (if available) for double log
        if (params) {
            jclass pcls = env->GetObjectClass(params);
            jclass cc = env->FindClass("java/lang/Class");
            if (cc && pcls && !env->ExceptionCheck()) {
                jmethodID gd = env->GetMethodID(cc, "getDeclaredMethods", "()[Ljava/lang/reflect/Method;");
                if (gd) {
                    jobjectArray ms = (jobjectArray)env->CallObjectMethod(pcls, gd);
                    if (!env->ExceptionCheck() && ms) {
                        int ml = env->GetArrayLength(ms);
                        // just log that we have params object, detailed extraction already done in query
                        LOGI("BUY_LOG: BillingFlowParams has %d methods, params=%p", ml, params);
                        env->DeleteLocalRef(ms);
                    } else env->ExceptionClear();
                }
                env->DeleteLocalRef(cc);
            } else env->ExceptionClear();
            env->DeleteLocalRef(pcls);
        }
        bool delivered = false;
        if (env && original_this && g_cls_bci) {
            jclass bciCls = env->GetObjectClass(original_this);
            jclass classCls = env->FindClass("java/lang/Class");
            jmethodID getDeclaredFields = classCls ? env->GetMethodID(classCls, "getDeclaredFields", "()[Ljava/lang/reflect/Field;") : nullptr;
            jobjectArray fields = getDeclaredFields ? (jobjectArray)env->CallObjectMethod(bciCls, getDeclaredFields) : nullptr;
            if (env->ExceptionCheck()) { env->ExceptionClear(); fields = nullptr; }
            int fcount = fields ? env->GetArrayLength(fields) : 0;
            LOGI("launchBillingFlow: scanning %d fields of BillingClientImpl", fcount);
            jclass fieldCls = env->FindClass("java/lang/reflect/Field");
            jmethodID getNameMid = fieldCls ? env->GetMethodID(fieldCls, "getName", "()Ljava/lang/String;") : nullptr;
            jmethodID getTypeMid = fieldCls ? env->GetMethodID(fieldCls, "getType", "()Ljava/lang/Class;") : nullptr;
            jmethodID getMid = fieldCls ? env->GetMethodID(fieldCls, "get", "(Ljava/lang/Object;)Ljava/lang/Object;") : nullptr;
            jmethodID setAccMid = fieldCls ? env->GetMethodID(fieldCls, "setAccessible", "(Z)V") : nullptr;
            jmethodID getTypeNameMid = classCls ? env->GetMethodID(classCls, "getName", "()Ljava/lang/String;") : nullptr;
            jclass pulCls = env->FindClass("com/android/billingclient/api/PurchasesUpdatedListener");
            if (env->ExceptionCheck()) env->ExceptionClear();
            if (!pulCls) pulCls = load_app_class(env, "com/android/billingclient/api/PurchasesUpdatedListener");
            if (env->ExceptionCheck()) env->ExceptionClear();
            for (int fi=0; fi<fcount && !delivered; fi++) {
                jobject field = env->GetObjectArrayElement(fields, fi);
                if (!field) continue;
                jstring fnameJ = getNameMid ? (jstring)env->CallObjectMethod(field, getNameMid) : nullptr;
                jobject ftype = getTypeMid ? env->CallObjectMethod(field, getTypeMid) : nullptr;
                const char* fnameC = fnameJ ? env->GetStringUTFChars(fnameJ, nullptr) : nullptr;
                jstring ftypeNameJ = ftype && getTypeNameMid ? (jstring)env->CallObjectMethod(ftype, getTypeNameMid) : nullptr;
                const char* ftypeC = ftypeNameJ ? env->GetStringUTFChars(ftypeNameJ, nullptr) : nullptr;
                LOGI("launchBillingFlow: scan field[%d/%d] %s (%s)", fi, fcount, fnameC?fnameC:"?", ftypeC?ftypeC:"?");
                if (setAccMid) { env->CallVoidMethod(field, setAccMid, JNI_TRUE); if (env->ExceptionCheck()) env->ExceptionClear(); }
                jobject fval = nullptr;
                if (getMid) {
                    fval = env->CallObjectMethod(field, getMid, original_this);
                    if (env->ExceptionCheck()) { env->ExceptionClear(); fval = nullptr; }
                }
                jobject listener = nullptr;
                bool isDirect = false;
                if (fval && pulCls && env->IsInstanceOf(fval, pulCls)) {
                    listener = fval;
                    isDirect = true;
                    LOGI("launchBillingFlow: field %s (%s) is direct PurchasesUpdatedListener %p", fnameC?fnameC:"?", ftypeC?ftypeC:"?", listener);
                } else if (fval) {
                    jclass fvalCls = env->GetObjectClass(fval);
                    jmethodID getDeclaredMethodsMid = classCls ? env->GetMethodID(classCls, "getDeclaredMethods", "()[Ljava/lang/reflect/Method;") : nullptr;
                    if (env->ExceptionCheck()) env->ExceptionClear();
                    jobjectArray mths = getDeclaredMethodsMid ? (jobjectArray)env->CallObjectMethod(fvalCls, getDeclaredMethodsMid) : nullptr;
                    if (env->ExceptionCheck()) { env->ExceptionClear(); mths = nullptr; }
                    if (mths) {
                        int mlen = env->GetArrayLength(mths);
                        jclass methodCls = env->FindClass("java/lang/reflect/Method");
                        if (env->ExceptionCheck()) env->ExceptionClear();
                        jmethodID mGetRet = methodCls ? env->GetMethodID(methodCls, "getReturnType", "()Ljava/lang/Class;") : nullptr;
                        jmethodID mInvoke = methodCls ? env->GetMethodID(methodCls, "invoke", "(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;") : nullptr;
                        jmethodID mSetAcc = methodCls ? env->GetMethodID(methodCls, "setAccessible", "(Z)V") : nullptr;
                        for (int mi=0; mi<mlen && !listener; mi++) {
                            jobject m = env->GetObjectArrayElement(mths, mi);
                            if (mSetAcc) { env->CallVoidMethod(m, mSetAcc, JNI_TRUE); if (env->ExceptionCheck()) env->ExceptionClear(); }
                            jobject retType = mGetRet ? env->CallObjectMethod(m, mGetRet) : nullptr;
                            if (env->ExceptionCheck()) { env->ExceptionClear(); retType = nullptr; }
                            jstring retNameJ = retType && getTypeNameMid ? (jstring)env->CallObjectMethod(retType, getTypeNameMid) : nullptr;
                            if (env->ExceptionCheck()) { env->ExceptionClear(); retNameJ = nullptr; }
                            const char* retNameC = retNameJ ? env->GetStringUTFChars(retNameJ, nullptr) : nullptr;
                            // SOLO getter che restituiscono esattamente PurchasesUpdatedListener vengono invocati.
                            // Un invoke generico 0-arg su field come zzbw/provider gms = chiamata Binder sincrona
                            // sul main thread -> HANG del gioco (osservato: freeze dopo la scan dei 37 field).
                            // Il getter del listener (es. zzf/zze) ha quel return type ed e' un accesso banale.
                            jmethodID getParamCountMid = methodCls ? env->GetMethodID(methodCls, "getParameterCount", "()I") : nullptr;
                            int pcnt = -1;
                            if (getParamCountMid) { pcnt = env->CallIntMethod(m, getParamCountMid); if (env->ExceptionCheck()) { env->ExceptionClear(); pcnt = -1; } }
                            bool retIsPUL = retNameC && strcmp(retNameC, "com.android.billingclient.api.PurchasesUpdatedListener") == 0;
                            bool tryInvoke = (pcnt == 0) && retIsPUL && !listener;
                            if (tryInvoke) {
                                jobject empty = env->NewObjectArray(0, env->FindClass("java/lang/Object"), nullptr);
                                if (env->ExceptionCheck()) env->ExceptionClear();
                                jobject res = mInvoke ? env->CallObjectMethod(m, mInvoke, fval, empty) : nullptr;
                                if (env->ExceptionCheck()) { env->ExceptionClear(); res = nullptr; }
                                if (res && pulCls && env->IsInstanceOf(res, pulCls)) {
                                    listener = res;
                                    LOGI("launchBillingFlow: wrapper %s via %s -> listener %p", fnameC?fnameC:"?", retNameC?retNameC:"unknown", listener);
                                } else if (res) env->DeleteLocalRef(res);
                                if (empty) env->DeleteLocalRef(empty);
                            }
                            if (retNameC) env->ReleaseStringUTFChars(retNameJ, retNameC);
                            if (retNameJ) env->DeleteLocalRef(retNameJ);
                            if (retType) env->DeleteLocalRef(retType);
                            env->DeleteLocalRef(m);
                        }
                        if (methodCls) env->DeleteLocalRef(methodCls);
                        env->DeleteLocalRef(mths);
                    } else env->ExceptionClear();
                    env->DeleteLocalRef(fvalCls);
                    if (!listener) { env->DeleteLocalRef(fval); fval = nullptr; }
                }
                if (listener) {
                    jclass alCls = env->FindClass("java/util/ArrayList");
                    jmethodID alInit = alCls ? env->GetMethodID(alCls, "<init>", "()V") : nullptr;
                    jobject list = alInit ? env->NewObject(alCls, alInit) : nullptr;
                    if (list && fake && g_mid_list_add) { env->CallBooleanMethod(list, g_mid_list_add, fake); if (env->ExceptionCheck()) env->ExceptionClear(); }
                    jclass lisCls = env->GetObjectClass(listener);
                    jmethodID mOnPU = env->GetMethodID(lisCls, "onPurchasesUpdated", "(Lcom/android/billingclient/api/BillingResult;Ljava/util/List;)V");
                    if (env->ExceptionCheck()) env->ExceptionClear();
                    if (mOnPU) {
                        if (!br) { jobject tmp = make_success_billing_result(env); if (tmp) { if (br) env->DeleteGlobalRef(br); br = env->NewGlobalRef(tmp); env->DeleteLocalRef(tmp); } }
                        jclass cl = env->GetObjectClass(listener);
                        jstring cname = getTypeNameMid ? (jstring)env->CallObjectMethod(cl, getTypeNameMid) : nullptr;
                        const char* cn = cname ? env->GetStringUTFChars(cname, nullptr) : nullptr;
                        LOGI("launchBillingFlow: calling onPurchasesUpdated listener=%p class=%s via field %s", listener, cn?cn:"null", fnameC?fnameC:"?");
                        if (cn) env->ReleaseStringUTFChars(cname, cn);
                        if (cname) env->DeleteLocalRef(cname);
                        env->DeleteLocalRef(cl);
                        env->CallVoidMethod(listener, mOnPU, br, list);
                        if (env->ExceptionCheck()) { env->ExceptionDescribe(); env->ExceptionClear(); } else { LOGI("STORE_STEP: launchBillingFlow fake delivered via %s", fnameC?fnameC:"?"); delivered = true; }
                    } else LOGI("launchBillingFlow: onPurchasesUpdated not found on %s", fnameC?fnameC:"?");
                    if (list) env->DeleteLocalRef(list);
                    env->DeleteLocalRef(lisCls);
                    if (alCls) env->DeleteLocalRef(alCls);
                    if (!isDirect) {
                        env->DeleteLocalRef(listener);
                        if (fval) env->DeleteLocalRef(fval);
                    } else env->DeleteLocalRef(fval);
                }
                if (fnameC) env->ReleaseStringUTFChars(fnameJ, fnameC);
                if (ftypeC) env->ReleaseStringUTFChars(ftypeNameJ, ftypeC);
                if (fnameJ) env->DeleteLocalRef(fnameJ);
                if (ftypeNameJ) env->DeleteLocalRef(ftypeNameJ);
                if (ftype) env->DeleteLocalRef(ftype);
                env->DeleteLocalRef(field);
            }
            if (fields) env->DeleteLocalRef(fields);
            if (fieldCls) env->DeleteLocalRef(fieldCls);
            if (pulCls) {
                if (env->GetObjectRefType(pulCls) == JNIGlobalRefType) env->DeleteGlobalRef(pulCls);
                else env->DeleteLocalRef(pulCls);
            }
            env->DeleteLocalRef(bciCls);
        }
        // Keep fake and br global alive for app lifetime (prevent Scudo poison / GC)
        // if (fake) env->DeleteGlobalRef(fake); // intentionally leak
        // if (br) env->DeleteGlobalRef(br);
        // Fallback: try Unity StoreBilling proxy if direct zzf delivery failed or as extra
        if (!delivered) {
            // Try to deliver via PurchaseUpdatedListenerProxy directly by faking a call to its onPurchasesUpdated
            // Create a fake BillingResult and list with fake purchase, then try to find any PurchaseUpdatedListenerProxy instance via reflection on g_app_context
            LOGI("launchBillingFlow: trying StoreBilling fallback via proxy");
            // Search for field of type StoreBilling in Application (brute force)
            jclass appCls = env->GetObjectClass(g_app_context);
            // Try to get StoreBilling instance via StoreMonitor or similar - simplified: just log and try to call proxy directly via new instance
            jclass puProxyCls = g_cls_pu_proxy;
            if (puProxyCls) {
                // Create a dummy proxy and try to trigger its onPurchasesUpdated with our fake
                // Instead, we can directly call the game's PurchaseUpdatedResponseListener if we can find it
                LOGI("launchBillingFlow: pu_proxy class available, attempting direct proxy delivery");
                // For now, just create a new proxy with a dummy listener and try - not ideal but try to find real instance via app fields
                // Brute force: iterate fields of Application
                jclass cls = appCls;
                // Try to find Field of type StoreBilling
                // Use reflection to get declared fields
                jclass classCls = env->FindClass("java/lang/Class");
                jmethodID getDeclaredFields = env->GetMethodID(classCls, "getDeclaredFields", "()[Ljava/lang/reflect/Field;");
                jobjectArray fields = (jobjectArray)env->CallObjectMethod(appCls, getDeclaredFields);
                if (!env->ExceptionCheck() && fields) {
                    int len = env->GetArrayLength(fields);
                    LOGI("launchBillingFlow: app has %d declared fields", len);
                } else { env->ExceptionClear(); }
                if (fields) env->DeleteLocalRef(fields);
                env->DeleteLocalRef(classCls);
            }
            env->DeleteLocalRef(appCls);
            env->ExceptionClear();
        }
        // Return success BillingResult without calling original (which would launch Play)
        if (!br) br = make_success_billing_result(env);
        if (!br && activity) { // fallback: try to get via backup's original? just create dummy via builder failure -> return null?
            LOGE("launchBillingFlow: could not create BillingResult, returning null");
        }
        if (activity) env->DeleteLocalRef(activity);
        if (params) env->DeleteLocalRef(params);
        if (original_this) env->DeleteLocalRef(original_this);
        LOGI("launchBillingFlow: bypassed Play, delivered=%d", delivered);
        return br; // return BillingResult directly (boxed)
    } }
    case 9: { {
        jobject params = argc > 1 ? env->GetObjectArrayElement(args, 1) : nullptr;
        jobject listener = argc > 2 ? env->GetObjectArrayElement(args, 2) : nullptr;
        LOGI("acknowledgePurchase: intercepted, faking success");
        if (listener) {
            jclass lc = env->GetObjectClass(listener);
            jmethodID m = env->GetMethodID(lc, "onAcknowledgePurchaseResponse", "(Lcom/android/billingclient/api/BillingResult;)V");
            if (!m) { env->ExceptionClear(); m = env->GetMethodID(lc, "onAcknowledgePurchaseResponse", "(Lcom/android/billingclient/api/BillingResult;)V"); }
            if (m) {
                jobject br = make_success_billing_result(env);
                env->CallVoidMethod(listener, m, br);
                if (br) env->DeleteLocalRef(br);
                LOGI("acknowledgePurchase: fake success delivered");
                if (env->ExceptionCheck()) env->ExceptionClear();
            } else { env->ExceptionClear(); LOGE("acknowledgePurchase: listener method not found"); }
            env->DeleteLocalRef(lc);
        }
        if (params) env->DeleteLocalRef(params);
        if (listener) env->DeleteLocalRef(listener);
        if (original_this) env->DeleteLocalRef(original_this);
        return nullptr;
    } }
    case 10: { {
        jobject params = argc > 1 ? env->GetObjectArrayElement(args, 1) : nullptr;
        jobject listener = argc > 2 ? env->GetObjectArrayElement(args, 2) : nullptr;
        LOGI("consumeAsync: intercepted, faking success");
        if (listener) {
            jclass lc = env->GetObjectClass(listener);
            jmethodID m = env->GetMethodID(lc, "onConsumeResponse", "(Lcom/android/billingclient/api/BillingResult;Ljava/lang/String;)V");
            if (m) {
                jobject br = make_success_billing_result(env);
                jstring token = env->NewStringUTF("mod_token");
                env->CallVoidMethod(listener, m, br, token);
                if (br) env->DeleteLocalRef(br);
                env->DeleteLocalRef(token);
                LOGI("consumeAsync: fake success delivered");
                if (env->ExceptionCheck()) env->ExceptionClear();
            } else { env->ExceptionClear(); LOGE("consumeAsync: listener method not found"); }
            env->DeleteLocalRef(lc);
        }
        if (params) env->DeleteLocalRef(params);
        if (listener) env->DeleteLocalRef(listener);
        if (original_this) env->DeleteLocalRef(original_this);
        return nullptr;
    } }
    default:
        LOGE("nativeDispatch: unknown hookId=%d", hookId);
        return nullptr;
    }
}

static void install_art_hooks(JNIEnv *env) {
    if (!g_app_context) { LOGE("install_art_hooks: NO app context — aborting"); return; }
    LOGI("install_art_hooks: START (app_context=%p)", (void*)g_app_context);

    for (int attempt = 0; attempt < 12; attempt++) {
        LOGI("install_art_hooks: attempt %d/12", attempt + 1);
        bool missing = false;
        // deprecated optional classes (Play Billing <8.3.0) - not required, keep for log
        if (!g_cls_zzbm)      { g_cls_zzbm      = load_app_class(env, "com/android/billingclient/api/zzbm"); LOGI("  zzbm=%p (optional)", (void*)g_cls_zzbm); }
        if (!g_cls_bci)       { g_cls_bci       = load_app_class(env, "com/android/billingclient/api/BillingClientImpl"); LOGI("  bci=%p", (void*)g_cls_bci); }
        if (!g_cls_proxy)     { g_cls_proxy     = load_app_class(env, "com/android/billingclient/api/ProxyBillingActivity"); LOGI("  proxy=%p", (void*)g_cls_proxy); }
        if (!g_cls_gpbl_proxy){ g_cls_gpbl_proxy= load_app_class(env, "com/unity3d/services/store/gpbl/proxies/PurchasesResponseListenerProxy"); LOGI("  gpbl_proxy=%p", (void*)g_cls_gpbl_proxy); }
        if (!g_cls_pu_proxy)  { g_cls_pu_proxy  = load_app_class(env, "com/unity3d/services/store/gpbl/proxies/PurchaseUpdatedListenerProxy"); LOGI("  pu_proxy=%p", (void*)g_cls_pu_proxy); }
        if (!g_cls_max)       { g_cls_max       = load_app_class(env, "com/applovin/mediation/unity/MaxUnityAdManager"); LOGI("  max=%p", (void*)g_cls_max); }
        if (!g_cls_purchase)  { g_cls_purchase  = load_app_class(env, "com/android/billingclient/api/Purchase"); LOGI("  purchase=%p", (void*)g_cls_purchase); }
        if (!g_cls_zzcj)      { g_cls_zzcj      = load_app_class(env, "com/android/billingclient/api/zzdc"); LOGI("  zzdc=%p (optional deprecated)", (void*)g_cls_zzcj); }
        if (!g_cls_billingresult) { g_cls_billingresult = load_app_class(env, "com/android/billingclient/api/BillingResult"); LOGI("  billingresult=%p", (void*)g_cls_billingresult); }
        if (!g_cls_productdetails) { g_cls_productdetails = load_app_class(env, "com/android/billingclient/api/ProductDetails"); LOGI("  productDetails=%p", (void*)g_cls_productdetails); }
        if (!g_cls_queryresult) { g_cls_queryresult = load_app_class(env, "com/android/billingclient/api/QueryProductDetailsResult"); LOGI("  queryResult=%p", (void*)g_cls_queryresult); }
        if (!g_cls_arraylist) { g_cls_arraylist = load_app_class(env, "java/util/ArrayList"); LOGI("  arraylist=%p", (void*)g_cls_arraylist); }
        if (!g_cls_intent)    { g_cls_intent    = load_app_class(env, "android/content/Intent"); LOGI("  intent=%p", (void*)g_cls_intent); }
        if (!g_cls_jsonobject) { g_cls_jsonobject = load_app_class(env, "org/json/JSONObject"); LOGI("  jsonobject=%p", (void*)g_cls_jsonobject); }
        if (!g_cls_system)    { g_cls_system    = load_app_class(env, "java/lang/System"); LOGI("  system=%p", (void*)g_cls_system); }

        if (!g_cls_bci || !g_cls_proxy || !g_cls_gpbl_proxy || !g_cls_pu_proxy || !g_cls_max || !g_cls_purchase ||
            !g_cls_billingresult || !g_cls_productdetails || !g_cls_queryresult || !g_cls_arraylist || !g_cls_intent || !g_cls_jsonobject || !g_cls_system) {
            missing = true;
        }

        if (missing) {
            LOGI("install_art_hooks: some classes NOT ready, retry %d/12", attempt + 1);
            sleep(5);
            continue;
        }
        LOGI("install_art_hooks: all classes loaded OK");

        LOGI("install_art_hooks: resolving helper methods...");
        g_mid_purchase_init = env->GetMethodID(g_cls_purchase, "<init>", "(Ljava/lang/String;Ljava/lang/String;)V");
        LOGI("  purchase_init=%p", (void*)g_mid_purchase_init);
        jclass list_cls = env->FindClass("java/util/List");
        LOGI("  List.class=%p", (void*)list_cls);
        g_mid_list_add = env->GetMethodID(list_cls, "add", "(Ljava/lang/Object;)Z");
        LOGI("  list_add=%p", (void*)g_mid_list_add);
        g_mid_arraylist_init = env->GetMethodID(g_cls_arraylist, "<init>", "()V");
        LOGI("  arraylist_init=%p", (void*)g_mid_arraylist_init);
        g_mid_intent_putextra_int = env->GetMethodID(g_cls_intent, "putExtra", "(Ljava/lang/String;I)Landroid/content/Intent;");
        LOGI("  intent_putextra_int=%p", (void*)g_mid_intent_putextra_int);
        g_mid_intent_putsal = env->GetMethodID(g_cls_intent, "putStringArrayListExtra", "(Ljava/lang/String;Ljava/util/ArrayList;)Landroid/content/Intent;");
        LOGI("  intent_putsal=%p", (void*)g_mid_intent_putsal);
        g_mid_forward = env->GetStaticMethodID(g_cls_max, "forwardUnityEvent", "(Lorg/json/JSONObject;)V");
        LOGI("  max_forward=%p", (void*)g_mid_forward);
        g_mid_jsonobj_init = env->GetMethodID(g_cls_jsonobject, "<init>", "()V");
        LOGI("  jsonobj_init=%p", (void*)g_mid_jsonobj_init);
        g_mid_jsonobj_put = env->GetMethodID(g_cls_jsonobject, "put", "(Ljava/lang/String;Ljava/lang/Object;)Lorg/json/JSONObject;");
        LOGI("  jsonobj_put=%p", (void*)g_mid_jsonobj_put);
        g_mid_system_time = env->GetStaticMethodID(g_cls_system, "currentTimeMillis", "()J");
        LOGI("  system_time=%p", (void*)g_mid_system_time);
        g_cls_boolean = (jclass)env->NewGlobalRef(env->FindClass("java/lang/Boolean"));
        g_mid_boolean_valueof = env->GetStaticMethodID(g_cls_boolean, "valueOf", "(Z)Ljava/lang/Boolean;");
        LOGI("  boolean=%p valueOf=%p", (void*)g_cls_boolean, (void*)g_mid_boolean_valueof);
        // deprecated zzdc field - optional, try but not required
        if (g_cls_zzcj) {
            g_fid_zzcj_zzl = env->GetStaticFieldID(g_cls_zzcj, "zzi", "Lcom/android/billingclient/api/BillingResult;");
            LOGI("  zzdc_zzi=%p (optional)", (void*)g_fid_zzcj_zzl);
            env->ExceptionClear();
        } else {
            LOGI("  zzdc_zzi skipped (g_cls_zzcj null, optional)");
        }
        // BillingResult v8.3.0+ builder (required) - use app classloader (FindClass fails for app classes)
        g_mid_billing_newbuilder = env->GetStaticMethodID(g_cls_billingresult, "newBuilder", "()Lcom/android/billingclient/api/BillingResult$Builder;");
        LOGI("  billing_newBuilder=%p", (void*)g_mid_billing_newbuilder);
        if (!g_cls_billingbuilder) {
            g_cls_billingbuilder = load_app_class(env, "com/android/billingclient/api/BillingResult$Builder");
            LOGI("  billingbuilder load_app_class=%p", (void*)g_cls_billingbuilder);
        }
        if (g_cls_billingbuilder) {
            g_mid_builder_setResponseCode = env->GetMethodID(g_cls_billingbuilder, "setResponseCode", "(I)Lcom/android/billingclient/api/BillingResult$Builder;");
            g_mid_builder_setDebugMessage = env->GetMethodID(g_cls_billingbuilder, "setDebugMessage", "(Ljava/lang/String;)Lcom/android/billingclient/api/BillingResult$Builder;");
            g_mid_builder_build = env->GetMethodID(g_cls_billingbuilder, "build", "()Lcom/android/billingclient/api/BillingResult;");
            LOGI("  builder cls=%p setResponseCode=%p setDebugMessage=%p build=%p", (void*)g_cls_billingbuilder, (void*)g_mid_builder_setResponseCode, (void*)g_mid_builder_setDebugMessage, (void*)g_mid_builder_build);
        } else {
            env->ExceptionClear();
            LOGE("  BillingResult$Builder class NOT found via load_app_class");
        }
        env->ExceptionClear();
        if (!g_mid_purchase_init || !g_mid_list_add || !g_mid_arraylist_init ||
            !g_mid_intent_putextra_int || !g_mid_intent_putsal || !g_mid_forward ||
            !g_mid_jsonobj_init || !g_mid_jsonobj_put || !g_mid_system_time ||
            !g_mid_billing_newbuilder || !g_mid_builder_setResponseCode || !g_mid_builder_build ||
            !g_cls_boolean || !g_mid_boolean_valueof) {
            LOGE("install_art_hooks: some methods/fields MISSING — retry");
            LOGE("  purchase_init=%p list_add=%p arraylist_init=%p", (void*)g_mid_purchase_init, (void*)g_mid_list_add, (void*)g_mid_arraylist_init);
            LOGE("  intent_pi=%p intent_sal=%p forward=%p", (void*)g_mid_intent_putextra_int, (void*)g_mid_intent_putsal, (void*)g_mid_forward);
            LOGE("  json_init=%p json_put=%p sys_time=%p billing_newBuilder=%p builder_setRC=%p builder_build=%p bool=%p", (void*)g_mid_jsonobj_init, (void*)g_mid_jsonobj_put, (void*)g_mid_system_time, (void*)g_mid_billing_newbuilder, (void*)g_mid_builder_setResponseCode, (void*)g_mid_builder_build, (void*)g_cls_boolean);
            env->ExceptionClear();
            sleep(5);
            continue;
        }
        LOGI("install_art_hooks: all helper methods/fields resolved OK");

        cache_reflection();

        if (!lsplant_init(env)) {
            LOGE("install_art_hooks: lsplant_init FAILED");
            return;
        }

        jclass hc_cls = load_class(env, "com/drmod/HookerCallback");
        if (!hc_cls) { LOGE("HookerCallback class NOT found"); return; }
        JNINativeMethod nm[] = {
            {(char *)"nativeDispatch",
             (char *)"(ILcom/drmod/HookerCallback;[Ljava/lang/Object;)Ljava/lang/Object;",
             (void *)Java_com_drmod_HookerCallback_nativeDispatch}
        };
        if (env->RegisterNatives(hc_cls, nm, 1) != 0) {
            env->ExceptionClear();
            LOGE("install_art_hooks: RegisterNatives nativeDispatch failed");
        } else {
            LOGI("install_art_hooks: nativeDispatch registered");
        }
        g_hooker_cls = (jclass)env->NewGlobalRef(hc_cls);
        g_hooker_ctor = env->GetMethodID(hc_cls, "<init>", "(I)V");
        jmethodID cb_mid = env->GetMethodID(hc_cls, "callback", "([Ljava/lang/Object;)Ljava/lang/Object;");
        g_hooker_callback_method = env->ToReflectedMethod(hc_cls, cb_mid, false);
        g_hooker_callback_method = env->NewGlobalRef(g_hooker_callback_method);
        env->DeleteLocalRef(hc_cls);
        if (!g_hooker_ctor || !g_hooker_callback_method) {
            env->ExceptionClear();
            LOGE("install_art_hooks: HookerCallback ctor/callback NOT found");
            return;
        }
        LOGI("install_art_hooks: HookerCallback class OK (ctor=%p cb=%p)", (void*)g_hooker_ctor, (void*)g_hooker_callback_method);

        LOGI("install_art_hooks: installing hooks via LSPlant...");
        // Store: hook l'unico punto game-facing che riceve {BillingResult, List<Purchase>} nella
        // query path di Unity gpbl (PurchasesResponseListenerProxy). I vecchi hook su zzbm (Callable)
        // e BillingClientImpl.zzaI non esistono in Play Billing 8.3.0 → rimossi.
        g_backup_a = lsplant_hook_method(env, g_cls_gpbl_proxy, "onQueryPurchasesResponse",
            "(Ljava/lang/Object;Ljava/util/List;)V", false, 0);
        g_backup_g = lsplant_hook_method(env, g_cls_pu_proxy, "onPurchasesUpdated",
            "(Ljava/lang/Object;Ljava/util/List;)V", false, 7);
        g_backup_h = lsplant_hook_method(env, g_cls_bci, "launchBillingFlow",
            "(Landroid/app/Activity;Lcom/android/billingclient/api/BillingFlowParams;)Lcom/android/billingclient/api/BillingResult;", false, 8);
        g_backup_i = lsplant_hook_method(env, g_cls_bci, "acknowledgePurchase",
            "(Lcom/android/billingclient/api/AcknowledgePurchaseParams;Lcom/android/billingclient/api/AcknowledgePurchaseResponseListener;)V", false, 9);
        g_backup_j = lsplant_hook_method(env, g_cls_bci, "consumeAsync",
            "(Lcom/android/billingclient/api/ConsumeParams;Lcom/android/billingclient/api/ConsumeResponseListener;)V", false, 10);
        g_backup_d = lsplant_hook_method(env, g_cls_bci, "queryProductDetailsAsync",
            "(Lcom/android/billingclient/api/QueryProductDetailsParams;Lcom/android/billingclient/api/ProductDetailsResponseListener;)V", false, 3);
        g_backup_e = lsplant_hook_method(env, g_cls_proxy, "onActivityResult",
            "(IILandroid/content/Intent;)V", false, 4);
        lsplant_hook_method(env, g_cls_max, "showRewardedAd",
            "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V", false, 5);
        // Ad: forza isRewardedAdReady=true così il gioco procede in showRewardedAd (hook 5 -> reward).
        g_backup_f = lsplant_hook_method(env, g_cls_max, "isRewardedAdReady",
            "(Ljava/lang/String;)Z", false, 6);
        LOGI("install_art_hooks: ALL LSPlant HOOKS DONE");
        return;
    }
    LOGE("install_art_hooks: gave up after 12 attempts");
}

// ==================== Process verification ====================

static bool is_exact_target(JNIEnv *env, jstring nice_name) {
    if (!env || !nice_name)
        return false;

    const char *name = env->GetStringUTFChars(nice_name, nullptr);
    if (!name)
        return false;

    bool match = strcmp(name, GAME_PKG) == 0;

    LOGI("process=[%s], target=%d", name, match);

    env->ReleaseStringUTFChars(nice_name, name);
    return match;
}

static uintptr_t find_lib_base(const char *lib_name) {
    LOGI("find_lib_base: searching for %s", lib_name);
    FILE *f = fopen("/proc/self/maps", "r");
    if (!f) { LOGE("find_lib_base: fopen /proc/self/maps failed errno=%d (%s)", errno, strerror(errno)); return 0; }
    char line[512];
    uintptr_t best = 0;
    while (fgets(line, sizeof(line), f)) {
        if (!strstr(line, lib_name)) continue;
        uintptr_t start = 0;
        unsigned long foff = 0;
        char perms[8] = {};
        if (sscanf(line, "%lx-%*x %7s %lx", &start, perms, &foff) != 3) {
            LOGE("find_lib_base: sscanf FAILED on line: %.200s", line);
            continue;
        }
        LOGI("find_lib_base: mapping %s start=0x%lx perms=%s foff=0x%lx", lib_name, (unsigned long)start, perms, foff);
        if (strchr(perms, 'x')) {
            uintptr_t b = start - (uintptr_t)foff;
            LOGI("find_lib_base: exec mapping 0x%lx foff 0x%lx -> base 0x%lx",
                 (unsigned long)start, foff, (unsigned long)b);
            fclose(f);
            return b;
        }
        if (foff == 0 && start && (best == 0 || start < best)) best = start;
    }
    fclose(f);
    LOGI("find_lib_base: no exec mapping found, fallback best=0x%lx", (unsigned long)best);
    return best;
}

static bool is_target_process() {
    char name[256] = {};
    FILE *f = fopen("/proc/self/cmdline", "r");
    if (!f) { LOGE("is_target_process: fopen /proc/self/cmdline failed errno=%d (%s)", errno, strerror(errno)); return false; }
    size_t n = fread(name, 1, sizeof(name) - 1, f);
    fclose(f);
    if (n == 0) { LOGE("is_target_process: cmdline read returned 0 bytes"); return false; }
    name[n] = '\0';
    bool match = strcmp(name, GAME_PKG_FULL) == 0;
    LOGI("is_target_process: cmdline=[%s] match=%d", name, match);
    return match;
}

// ==================== Boot marker ====================

// ==================== Worker ====================

static void *worker(void *) {
    if (!g_is_game) {
        LOGE("Worker refused: not target");
        return nullptr;
    }
    LOGI("Worker confirmed target process, delay %ds", DELAY_SEC);

    sleep(DELAY_SEC);
    LOGI("Worker: delay elapsed, starting work");

    g_immortality   = false;
    g_forced_crits  = false;
    g_minigame_loot = false;
    g_divine_forge  = false;
    g_overdrive_divine = false;
    g_forge_exp     = false;
    LOGI("Worker: toggle defaults FORCED OFF - imm=%d crit=%d forge=%d od=%d mg=%d fexp=%d",
         g_immortality, g_forced_crits, g_divine_forge, g_overdrive_divine, g_minigame_loot, g_forge_exp);
    LOGI("Worker: toggle addrs - imm=%p crit=%p forge=%p od=%p mg=%p fexp=%p",
         &g_immortality, &g_forced_crits, &g_divine_forge, &g_overdrive_divine, &g_minigame_loot, &g_forge_exp);

    uintptr_t base = 0;
    for (int r = 0; r < 12; r++) {
        base = find_lib_base("libil2cpp.so");
        if (base) break;
        LOGI("Worker: libil2cpp not found, retry %d/12 in 5s", r+1);
        sleep(5);
    }
    if (!base) { LOGE("libil2cpp.so not found after retries"); return nullptr; }
    g_il2cpp_base = base;
    LOGI("libil2cpp base: %p", (void *)base);

    LOGI("Worker: startup restore — checking toggleable patches already applied...");
    int restored = 0;
    for (int i = 0; i < num_patches; i++) {
        if (!patches[i].toggleable || !patches[i].repl || !patches[i].orig) continue;
        uintptr_t a = base + patches[i].off;
        if (memcmp((void *)a, patches[i].repl, patches[i].len) == 0) {
            LOGI("  RESTORE: %s @0x%X already patched, reverting to original", patches[i].name, patches[i].off);
            if (unpatch_bytes(base, patches[i])) { restored++; LOGI("  RESTORE OK: %s", patches[i].name); }
            else { LOGE("  RESTORE FAIL: %s", patches[i].name); }
        }
    }
    LOGI("Worker: startup restore DONE — %d reverted", restored);

    LOGI("Worker: applying byte patches (always-on only)...");
    int ok = 0, fail = 0;
    for (int i = 0; i < num_patches; i++) {
        if (patches[i].toggleable) continue;
        if (patch_bytes(base, patches[i])) ok++; else fail++;
    }
    LOGI("Worker: byte patches DONE — %d ok, %d fail", ok, fail);

    LOGI("Worker: installing immortality hook...");
    install_immortality(base);

    LOGI("Worker: installing store hook (ProcessPurchase -> calls ORIGINAL so grant runs)");
    install_store_hook(base);
    LOGI("Worker: installing dnf hook (dnf -> returns true, passes verification)");
    install_dnf_hook(base);
    LOGI("Worker: installing tbz fix #1 (ProcessPurchase GATE1 entry+0x118 -> NOP, fall-through a path TRUE che spara l'evento Action<Product> = GRANT)");
    install_tbz_force_grant();
    LOGI("Worker: installing tbz fix #2 (ProcessPurchase GATE2 entry+0x1D0 -> unconditional b to GRANT PATH con eao/dne; NON raggiungibile con GATE1=NOP)");
    install_fallback_grant_force();
    LOGI("Worker: installing dsq redirect (v4.77: dsq@0x28B68A4 -> b dsu@0x28B6924: la callback del path TRUE esegue il GRANT COMPLETO, non il wrapper vuoto)");
    install_dsq_redirect();
    LOGI("Worker: installing club bridge (v4.78: entry hook su dsu@0x28B6924 -> attiva ClubTier localmente via esj per vip/elite club)");
    install_club_bridge();
    LOGI("Worker: installing STORE CHAIN FULL TRACE (v4.71: logga OGNI movimento della catena store in tempo reale)");
    install_store_chain_trace();
    LOGI("Worker: smart-grant CHECK bypass DISABLED in v56 (non applicabile: v56 non ha chain validator; metodo finisce ~entry+0x2D8)");
    // install_smart_grant_check_nop();
    LOGI("Worker: dxn entry trace DISABLED in v56 (off 0x28BE238 era v55; in v56 il grant avviene dentro ProcessPurchase via eao 0x2991924)");
    // install_dxn_entry_trace();
    LOGI("Worker: x20-gate probe DISABLED in v56 (offset entry+0x90 era v55)");
    // install_x20_gate_probe();
    LOGI("Worker: grant chain probes DISABLED in v56 (offsets v55: fallback entry 0x28a92e8, chain 0x298f448/0x28a8f1c/0x28a94d4/0x28bc3e8)");
    // install_fallback_entry_probe();
    // install_grant_chain_probes();
    LOGI("Worker: engine dispatch probe DISABLED in v56 (0x2a7ed18 era v55)");
    // install_engine_dispatch_probe();
    LOGI("Worker: POST trace probes DISABLED in v56 (dli 0x27dabbc / dze 0x298f548 erano v55)");
    // install_post_trace_probes();
    LOGI("Worker: grant callback invoke probe DISABLED in v56 (0x2379940 era v55)");
    // install_grant_callback_invoke_probe();
    LOGI("Worker: fc.dwu + apply-grant trace probes DISABLED in v56 (0x2A7C700 / 0x28bc158 erano v55)");
    // install_fc_dwu_grant_trace_probes();
    LOGI("Worker: VERIFY IAP probes + Code=0 FORCE DISABLED in v56 (verifyiap 0x28ad5c4 / dnv 0x28ad894 / bl patch 0x28ad928 erano v55)");
    // install_verify_iap_probes();
    LOGI("Worker: google purchase hook DISABLED (ART launchBillingFlow already handles, native interferes)");
    // GooglePurchaseUpdatedListener hook disabled
    LOGI("Worker: dw store REAL hooks DISABLED (overwrites g_orig vars per method -> wrong trampoline, causes crashes)");
    // install_dw_store_real_hooks(base);

    LOGI("Worker: installing User.eyu hook (dungeon keys A/B/C: chiavi mai giu', ad x%d, ads illimitate)", KEY_BONUS_MULT);
    install_user_eyu_probe();

    LOGI("Worker: granting SYSTEM_ALERT_WINDOW...");
    int ret = system("/vendor/bin/su -c 'appops set " GAME_PKG_FULL " SYSTEM_ALERT_WINDOW allow'");
    LOGI("SYSTEM_ALERT_WINDOW grant: ret=%d", ret);

    LOGI("Worker: starting JNI phase...");
    {
        JNIEnv *env = nullptr;
        if (g_jvm->AttachCurrentThread(&env, nullptr) != 0 || !env) {
            LOGE("worker: AttachCurrentThread failed");
        } else {
            LOGI("worker: JNI attached (env=%p)", (void*)env);
            jclass at_cls = env->FindClass("android/app/ActivityThread");
            LOGI("worker: ActivityThread.class=%p", (void*)at_cls);
            if (at_cls) {
                jmethodID current_at = env->GetStaticMethodID(at_cls,
                    "currentActivityThread", "()Landroid/app/ActivityThread;");
                jobject at = env->CallStaticObjectMethod(at_cls, current_at);
                if (at) {
                    jmethodID get_app = env->GetMethodID(at_cls,
                        "getApplication", "()Landroid/app/Application;");
                    jobject app = env->CallObjectMethod(at, get_app);
                    if (app) {
                        g_app_context = env->NewGlobalRef(app);
                        env->DeleteLocalRef(app);
                        LOGI("App context captured");
                    } else {
                        env->ExceptionClear();
                        LOGE("worker: getApplication returned null");
                    }
                    env->DeleteLocalRef(at);
                } else {
                    env->ExceptionClear();
                    LOGE("worker: currentActivityThread returned null");
                }
                env->DeleteLocalRef(at_cls);
            } else {
                env->ExceptionClear();
                LOGE("worker: ActivityThread not found");
            }

            LOGI("worker: calling load_dex...");
            load_dex(env);

            LOGI("worker: calling install_art_hooks...");
            install_art_hooks(env);

            LOGI("worker: calling create_overlay...");
            create_overlay(env);
            LOGI("worker: create_overlay done, detaching JNI...");
            g_jvm->DetachCurrentThread();
            LOGI("worker: JNI detached — all worker steps complete");
        }
    }

    g_activated = true;
    LOGI("All mods active");
    return nullptr;
}

// ==================== Zygisk module ====================

class DRMod : public zygisk::ModuleBase {
public:
    void onLoad(zygisk::Api *api, JNIEnv *env) override {
        this->api = api;
        this->env = env;
        env->GetJavaVM(&g_jvm);
        LOGI("onLoad");
    }

    void preAppSpecialize(zygisk::AppSpecializeArgs *args) override {
        g_is_game = false;
        g_activated = false;
        g_il2cpp_base = 0;
        g_main = nullptr;
        g_orig_fop = nullptr;
        g_dex_loader = nullptr;
        g_immortality   = false;
        g_forced_crits  = false;
        g_minigame_loot = false;
        g_divine_forge  = false;
        g_overdrive_divine = false;
        g_forge_exp     = false;

        if (!args || !args->nice_name) {
            LOGI("preAppSpecialize: no nice_name, dlclose");
            api->setOption(zygisk::DLCLOSE_MODULE_LIBRARY);
            return;
        }

        if (!is_exact_target(env, args->nice_name)) {
            LOGI("preAppSpecialize: not target, dlclose");
            api->setOption(zygisk::DLCLOSE_MODULE_LIBRARY);
            return;
        }

        g_is_game = true;
        LOGI("TARGET: %s", GAME_PKG);
    }

    void postAppSpecialize(const zygisk::AppSpecializeArgs *) override {
        if (!g_is_game) {
            // No force-stop here: it would run in every app process.
            LOGI("postAppSpecialize: not target, returning");
            return;
        }

        LOGI("postAppSpecialize: target process");

        pthread_t t;
        if (pthread_create(&t, nullptr, worker, nullptr) == 0) {
            pthread_detach(t);
            LOGI("postAppSpecialize: worker thread created");
        } else {
            LOGE("pthread_create failed");
        }
    }

    void preServerSpecialize(zygisk::ServerSpecializeArgs *args) override {
        LOGI("preServerSpecialize");
        (void)args;
    }

    void postServerSpecialize(const zygisk::ServerSpecializeArgs *args) override {
        LOGI("postServerSpecialize");
        (void)args;
    }

private:
    zygisk::Api *api = nullptr;
    JNIEnv *env = nullptr;
};

REGISTER_ZYGISK_MODULE(DRMod)
