// ==UserScript== // @name 仙山小农 油猴砍树挖矿 + 农场 + 抓包 // @namespace xsxn.capture // @version 3.0 // @description 自动砍树/挖矿/开垦/播种/收获;保留抓包导出 // @match https://xsxncdn.xdyouxi.net/* // @run-at document-start // @grant GM_setClipboard // @grant GM_xmlhttpRequest // @grant GM_registerMenuCommand // @grant unsafeWindow // @connect 127.0.0.1 // @connect localhost // ==/UserScript== (function () { 'use strict'; const PW = (typeof unsafeWindow !== 'undefined') ? unsafeWindow : window; // 页面真实 window // ============================================================ // 第一部分:document-start 钩 cc._RF,抓 AppMain.app(模块注册表) // ============================================================ window.__appMod = null; // AppMain 模块对象(.exports.AppMain.app 即注册表宿主) let __realCC = undefined; function hookRF(cc) { try { if (!cc || !cc._RF || cc._RF.__xsxn_hooked) return; cc._RF.__xsxn_hooked = true; const push = cc._RF.push; cc._RF.push = function (mod, uuid, name) { if (name === 'AppMain' && mod && !window.__appMod) window.__appMod = mod; return push.apply(this, arguments); }; log('[hook] cc._RF.push 已挂钩'); } catch (e) { } } // 拦截 window.cc 赋值(引擎脚本晚于本脚本执行) try { Object.defineProperty(window, 'cc', { configurable: true, get: function () { return __realCC; }, set: function (v) { __realCC = v; hookRF(v); }, }); } catch (e) { } function getApp() { // 首选:browserify 全局 __require(bundle 头部暴露 window.__require) try { const rq = PW.__require; if (typeof rq === 'function') { let mmObj = null; try { mmObj = rq('./A_ModuleManager/ModuleMap'); } catch (e) { } if (mmObj && mmObj.m) return { moduleMap: mmObj.m }; try { const A = rq('AppMain'); if (A && A.AppMain && A.AppMain.app && A.AppMain.app.moduleMap) return A.AppMain.app; } catch (e) { } } } catch (e) { } // 备选:cc._RF 钩子抓到的模块 const m = PW.__appMod || window.__appMod; if (!m) return null; const A = (m.exports && m.exports.AppMain) || m.AppMain; if (A && A.app && A.app.moduleMap) return A.app; return null; } // ============================================================ // 第二部分:日志与悬浮面板 // ============================================================ const LOG_LINES = []; let logBox = null; function log(s) { const t = new Date().toTimeString().slice(0, 8); LOG_LINES.push('[' + t + '] ' + s); if (LOG_LINES.length > 200) LOG_LINES.shift(); console.log('[仙山小农]', s); if (logBox) logBox.textContent = LOG_LINES.slice(-14).join('\n'); } function addUI() { if (document.getElementById('xsxn_panel')) return; const p = document.createElement('div'); p.id = 'xsxn_panel'; p.style.cssText = 'position:fixed;right:6px;bottom:34px;z-index:999999;background:rgba(0,0,0,.82);color:#0f0;' + 'font:12px/1.5 monospace;padding:8px;border:1px solid #0f0;border-radius:6px;max-width:290px;touch-action:none;'; p.innerHTML = '
≡ 拖动 ≡
' + '
' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '
' + '
' + '' + '' + '' + '' + '' + '
';
        document.body.appendChild(p);
        logBox = p.querySelector('#xsxn_log');
        logBox.textContent = LOG_LINES.slice(-14).join('\n');

        document.getElementById('xsxn_scan').onclick = function () { log('资源: ' + JSON.stringify(listChores())); };
        document.getElementById('xsqn_export').onclick = doExport;
        // 内存修改器面板:输入数值 -> 搜 / 滤 / 写 / 清
        function memVal() {
            const v = document.getElementById('xsxn_memv').value;
            if (v === '') { log('内存修改器:请先在输入框填数值'); return null; }
            return +v;
        }
        document.getElementById('xsxn_memsearch').onclick = function () { const v = memVal(); if (v !== null) memSearch(v); };
        document.getElementById('xsxn_memfilter').onclick = function () { const v = memVal(); if (v !== null) memFilter(v); };
        document.getElementById('xsxn_memwrite').onclick = function () { const v = memVal(); if (v !== null) memWrite(v); };
        document.getElementById('xsxn_memclear').onclick = function () { memClear(); };
        // 一键领奖:邮件全部领取 + 可提交任务全部提交
        function claimAll() {
            const mm = MM();
            let mailOk = 0, mailSkip = 0;
            const md = mm.MAINDATA || mm.MAILDATA || mm.MailData;
            if (md && md.getMailList && md.getMailAward) {
                let list = [];
                try { list = md.getMailList() || []; } catch (e) { }
                if (list.length && Array.isArray(list[0])) list = list[0]; // 兼容分组结构
                for (const m of list) {
                    try {
                        if (!m || !m.award || m.award === '' || m.state === 2) { mailSkip++; continue; } // 2=READGET
                        const r = md.getMailAward(m, false);
                        if (r !== false) mailOk++; else mailSkip++;
                    } catch (e) { mailSkip++; }
                }
            }
            let taskOk = 0, taskSkip = 0;
            const tm = mm.TASKMANAGER;
            if (tm && typeof tm.getHaves === 'function') {
                let tasks = [];
                try { tasks = tm.getHaves() || []; } catch (e) { }
                for (const t of tasks) {
                    try {
                        if (!t || !t.task_id) continue;
                        if (typeof t.canSubmit === 'function' && !t.canSubmit()) { taskSkip++; continue; }
                        if (typeof t.submit === 'function') { t.submit(); taskOk++; }
                        else if (typeof tm.submitTask === 'function' && tm.submitTask(t.task_id)) taskOk++; else taskSkip++;
                    } catch (e) { taskSkip++; }
                }
            }
            log('一键领奖: 邮件 +' + mailOk + ' (跳' + mailSkip + '),任务 +' + taskOk + ' (跳' + taskSkip + ')' +
                ((!md && !tm) ? '(本场景无邮件/任务模块)' : ''));
        }
        document.getElementById('xsxn_claim').onclick = function () { claimAll(); };
        // ==================== 守卫飞艇(TowerDefense)日常 ====================
        // 协议(逆向自 main_bundle.js):
        //   getChallengeList   -> 关卡列表 {stage_id, lv, type, is_passed, star}
        //   getDailyChances    -> 领每日挑战次数(奖励发 10189 门票)
        //   saodangChallenge   -> {stage_id, saodang_count} 仅 3 星通关关可扫
        //   getBattlePassRecord / claimBattlePassReward -> 令牌领取(status==2 可领)
        function tdNotice(mm) {
            return mm.NEW_NET_NOTICE || (mm.EnumClass && mm.EnumClass.NEW_NET_NOTICE) || null;
        }
        function tdPost(mm) {
            const rm = mm.REQ_METHOD || (mm.EnumClass && mm.EnumClass.REQ_METHOD);
            return (rm && rm.POST) || 'POST';
        }
        function tdActId(mm) {
            const a = mm.CONFIG && Number(mm.CONFIG.TFActId);
            return (a > 0) ? a : 4161;
        }
        // 接口数字 id(逆向自 EnumClass.NEW_NET_NOTICE,moduleMap 上拿不到枚举,直接用数字)
        const TD_NOTICE = { GETDAILYCHANCES: 494, GETDAILYCHANCESRECORD: 495, SAODANG: 496, REPORT: 497, CHALLENGELIST: 498, BATTLEPASS: 499, CLAIMBATTLEPASS: 500 };
        const TC_NOTICE = { ZAOTAI: 60, DAILY: 61, REPORT: 63, SHUFFLE: 64, PROGRESS: 70, FINISH: 69 };
        function tdApi(mm, noticeId, params, cb) {
            try {
                mm.APIHANDLER.apiRequestByURL(Object.assign({ activity_id: tdActId(mm), useJSONType: 1 }, params),
                    noticeId, tdPost(mm), function (r) {
                        log('飞艇 ' + noticeId + ': ret=' + (r && r.ret) + fmtReward(r));
                        cb && cb(r);
                    });
            } catch (e) { log('飞艇 err: ' + e.message); }
        }
        function fmtReward(r) {
            try {
                const d = r && r.data;
                const wc = d && d.waiting_collect;
                if (wc && Object.keys(wc).length) return ' 奖励:' + JSON.stringify(wc);
            } catch (e) { }
            return '';
        }
        function tdDaily(count) {
            const mm = MM();
            if (!mm || !mm.APIHANDLER) { log('飞艇: 游戏模块未就绪,稍后再试'); return; }
            count = count || 3;
            tdApi(mm, TD_NOTICE.GETDAILYCHANCES, {}, function (r1) {           // 1. 领每日次数
                tdApi(mm, TD_NOTICE.CHALLENGELIST, {}, function (r2) {      // 2. 关卡列表
                    let best = null;
                    const list = (r2 && r2.ret === 1 && r2.data) || [];
                    for (const s of list) {
                        if (Number(s.type) === 0 && Number(s.is_passed) === 1 && Number(s.star) >= 3
                            && (!best || Number(s.lv) > Number(best.lv))) best = s;
                    }
                    if (!best) { log('飞艇: 暂无可扫荡关卡(需3星通关)'); return; }
                    log('飞艇: 扫荡 ' + best.stage_id + ' x' + count);
                    tdApi(mm, TD_NOTICE.SAODANG,                      // 3. 扫荡最高3星关
                        { stage_id: Number(best.stage_id), saodang_count: count }, function (r3) {
                            if (!r3 || r3.ret !== 1) return;
                            tdApi(mm, TD_NOTICE.BATTLEPASS, {}, function (r4) { // 4. 令牌奖励
                                const zs = (r4 && r4.ret === 1 && r4.data && r4.data.zhiling_list) || [];
                                const claimable = zs.filter(function (z) { return z.status === 2; });
                                log('飞艇令牌: 可领 ' + claimable.length + ' 档');
                                claimable.forEach(function (z, i) {
                                    setTimeout(function () {
                                        tdApi(mm, TD_NOTICE.CLAIMBATTLEPASS, {});
                                    }, i * 800);
                                });
                            });
                        });
                });
            });
        }
        document.getElementById('xsxn_td').onclick = function () { tdDaily(3); };
        // ==================== 仙山食韵(ActTwoCombine 合成)自动 ====================
        // 协议:getZaotaiLayOut -> {layout:[等级], next_ball_level, current_score, ...}
        //       reportChallenge {ball_lv, insert_side, is_closed, location:{x,y}, reference_index} -> 服务器权威新布局
        //       每次发射耗 1 灶火;getDailyChances 领每日次数
        function tcActId(mm) {
            // 从活动数据表里按 type=MERGE_GAME(40) 找 activity_id
            try {
                const ad = mm.ACTIVITYDATA;
                const src = (ad.activity_datas || ad.activity_temp_datas) || {};
                for (const k in src) {
                    for (const k2 in src[k]) {
                        const a = src[k][k2];
                        if (a && a.getActivityType && Number(a.getActivityType()) === 40) {
                            const id = a.getActivityId && Number(a.getActivityId());
                            if (id > 0) return id;
                        }
                    }
                }
            } catch (e) { }
            return (mm.CONFIG && Number(mm.CONFIG.ActTwoCombineActId)) || 0;
        }
        function tcApi(mm, noticeId, params, cb) {
            try {
                mm.APIHANDLER.apiRequestByURL(Object.assign({ activity_id: tcActId(mm), useJSONType: 1 }, params),
                    noticeId, tdPost(mm), function (r) {
                        let extra = fmtReward(r);
                        if (r && r.ret !== 1) extra += ' raw=' + JSON.stringify(r).slice(0, 260);  // 失败时带出 msg
                        log('食韵 ' + noticeId + ': ret=' + (r && r.ret) + extra);
                        cb && cb(r);
                    });
            } catch (e) { log('食韵 err: ' + e.message); }
        }
        const TC = { running: false, count: 0, finishes: 0, finishFails: 0 };
        function tcStamina(mm) {
            try { return Number(mm.BALANCEDATA.getBalanceNum(10102)) || 0; } catch (e) { return -1; }
        }
        function tcShot(mm) {
            if (!TC.running) return;
            if (TC.count >= 120) { log('食韵: 达到单轮上限120次,停止'); TC.running = false; return; }
            const stam = tcStamina(mm);
            if (stam === 0) { log('食韵: 灶火(10102)耗尽,停止'); TC.running = false; return; }
            TC.count++;
            tcApi(mm, TC_NOTICE.ZAOTAI, {}, function (r) {
                if (!TC.running) return;
                if (!r || r.ret !== 1 || !r.data) { log('食韵: 布局获取失败,停止'); TC.running = false; return; }
                const d = r.data;
                const layout = Array.isArray(d.layout) ? d.layout.map(Number) : [];
                if (!layout.length) {
                    // 本局结束:finishGame 结算(返回新布局),再继续
                    if (TC.finishes >= 20) { log('食韵: 结算次数达上限,停止'); TC.running = false; return; }
                    TC.finishes++;
                    log('食韵: 盘面空(第' + TC.finishes + '局完),finishGame…');
                    tcApi(mm, TC_NOTICE.FINISH, {}, function (rf) {
                        if (!TC.running) return;
                        if (!rf || rf.ret !== 1) {
                            TC.finishFails++;
                            if (TC.finishFails >= 2) { log('食韵: finishGame 连续被拒,停止'); TC.running = false; return; }
                        } else TC.finishFails = 0;
                        setTimeout(function () { tcShot(mm); }, 500);
                    });
                    return;
                }
                const ballLv = Number(d.next_ball_level) || 1;
                // 贪心:找一个与发射球同级的水果,贴它插入并尝试闭环;否则插在下标0旁
                let refIdx = 0, closed = 0;
                for (let i = 0; i < layout.length; i++) {
                    if (layout[i] === ballLv) { refIdx = i; closed = 1; break; }
                }
                tcApi(mm, TC_NOTICE.REPORT, {
                    ball_lv: ballLv,
                    insert_side: 1,
                    is_closed: closed,
                    location: JSON.stringify({ x: 0, y: 0 }),
                    reference_index: refIdx,
                }, function (r2) {
                    if (!TC.running) return;
                    if (!r2 || r2.ret !== 1) {
                        log('食韵: 上报被拒 ret=' + (r2 && r2.ret) + ',重查布局');
                        setTimeout(function () { tcShot(mm); }, 900);  // 可能是灶火耗尽/盘面已空,交给下一轮判断
                        return;
                    }
                    const nd = r2.data || {};
                    const merged = Number(nd.last_merge_count) || 0;
                    log('食韵 第' + TC.count + '手: 合并x' + merged + ' 得分+' + (nd.score_delta || 0) +
                        ' 总分' + (nd.total_score != null ? nd.total_score : nd.current_score) + ' 灶火' + tcStamina(mm));
                    setTimeout(function () { tcShot(mm); }, 700);
                });
            });
        }
        document.getElementById('xsxn_tc').onclick = function () {
            if (TC.running) { TC.running = false; this.textContent = '食韵自动:关'; this.style.color = '#f80'; log('食韵自动: 停止'); return; }
            const mm = MM();
            if (!mm || !mm.APIHANDLER) { log('食韵: 游戏模块未就绪'); return; }
            TC.running = true; TC.count = 0; TC.finishes = 0;
            this.textContent = '食韵自动:开'; this.style.color = '#0f0';
            log('食韵自动: 开始(领次数->循环发射,再点一次停止)');
            tcApi(mm, TC_NOTICE.DAILY, {}, function () { tcShot(mm); });
        };
        // 飞艇战斗内 建塔/升级 0 消耗
        // 实际扣费链路(main_bundle @~11538874):h = scene.getTowerDefenseBuildCost(cost_num,type)
        //   → h>0 且 !CURDATA.hasItem(10192,h) 直接 return → CURDATA.useItem(10192,h)
        // 故需两处 patch:① Economy.normalizeCost(配置价归一)② Scene.prototype.getTowerDefenseBuildCost(折扣后实价)
        // 后者置 0 后 hasItem/useItem 均被跳过
        const TDFREE = { on: false, patched: [] };
        function tdFreeApply() {
            const mm = MM();
            if (!mm) return 0;
            let hit = 0;
            // 还原
            for (const p of TDFREE.patched) {
                try { Object.defineProperty(p.target, p.fn, p.desc || { value: p.orig, writable: true, configurable: true }); } catch (e) { }
            }
            TDFREE.patched = [];
            if (!TDFREE.on) return 0;
            const FNS = ['getTowerDefenseBuildCost', 'getUpgradeCost', 'getBuildCost']; // 不动 normalizeCost,保留卖塔回收
            const seen = new Set();
            function tryPatch(target) {
                if (!target || (typeof target !== 'object' && typeof target !== 'function')) return;
                if (seen.has(target)) return;
                seen.add(target);
                for (const fn of FNS) {
                    const desc = Object.getOwnPropertyDescriptor(target, fn);
                    if (!desc || typeof desc.value !== 'function' || !desc.configurable) continue;
                    const nd = Object.assign({}, desc);
                    nd.value = function () { return 0; };
                    try {
                        Object.defineProperty(target, fn, nd);
                        TDFREE.patched.push({ target: target, fn: fn, orig: desc.value, desc: desc });
                        hit++;
                    } catch (e) { }
                }
            }
            for (const key in mm) {
                try {
                    const mod = mm[key];
                    tryPatch(mod);
                    tryPatch(Object.getPrototypeOf(mod));
                    if (mod && mod.prototype) tryPatch(mod.prototype);
                } catch (e) { }
            }
            return hit;
        }
        document.getElementById('xsxn_tdfree').onclick = function () {
            TDFREE.on = !TDFREE.on;
            const hit = tdFreeApply();
            this.textContent = '飞艇0消耗:' + (TDFREE.on ? '开' : '关');
            this.style.color = TDFREE.on ? '#0f0' : '#0cf';
            log('飞艇建塔/升级 0消耗: ' + (TDFREE.on ? '开' : '关') + ',补丁 ' + hit + ' 处' + (hit ? '' : '(进守卫飞艇场景后再开)'));
        };
        // 自动宝箱 / 自动渔网 / 自动捕捉
        const AUTO2 = { box: { on: false }, net: { on: false }, catch: { on: false } };
        const ONCE_IN = {}; // 低频提示计数
        const TPED = {};    // 已拉到面前/已捕获的动物 id 记录
        const FISH_ST = {}; // 钓鱼状态计时 {since:抛竿时间, get:咬钩时间}
        function onceIn(n, key, fn) { ONCE_IN[key] = (ONCE_IN[key] || 0) + 1; if (ONCE_IN[key] >= n) { ONCE_IN[key] = 0; fn(); } }
        function mkAutoLoop(key, label, tick) {
            return function loop() {
                const st = AUTO2[key];
                if (!st.on) return;
                try { tick(); } catch (e) { log(label + ' err: ' + e.message); }
                setTimeout(loop, 3000);
            };
        }
        function isApiMatch(b, kws) {
            try {
                const mm = MM();
                const api = mm.ADATA.getItemApi(b.build_id) || {};
                const name = String(api.name || '');
                const ap = String(api.api || '');
                return kws.some(function (k) { return name.indexOf(k) >= 0 || ap.indexOf(k) >= 0; });
            } catch (e) { return false; }
        }
        // 自动宝箱:扫描场景建筑数据,treasureBOXes 里且未开(open!=1)的调 MAPOPERATION.openTreasure(真实开箱链路)
        const boxLoop = mkAutoLoop('box', '自动宝箱', function () {
            const mm = MM();
            const mo = mm.MAPOPERATION;
            if (!mo || !mo.openTreasure) return;
            const tbl = (mm.EnumClass && mm.EnumClass.treasureBOXes) || mm.treasureBOXes || [];
            const pools = [];
            try { if (mm.IHOUSEDATA && mm.IHOUSEDATA.sceneBuildData) pools.push(mm.IHOUSEDATA.sceneBuildData.getBuildingData()); } catch (e) { }
            try { if (mm.ISLANDDATA && mm.ISLANDDATA.sceneBuildData) pools.push(mm.ISLANDDATA.sceneBuildData.getBuildingData()); } catch (e) { }
            if (!pools.length) { try { const sc = mm.INFO.getCurrScene(); if (sc && sc.sceneBuildData) pools.push(sc.sceneBuildData.getBuildingData()); } catch (e) { } }
            const boxes = [];
            for (const pd of pools) {
                if (!pd) continue;
                for (const id in pd) {
                    const b = pd[id];
                    if (!b || !b.locate || !b.build_id || b.open == 1) continue;
                    if (tbl.length) { if (tbl.includes(Number(b.build_id))) boxes.push(b); }
                    else {
                        const api = mm.ADATA && mm.ADATA.getItemApi && mm.ADATA.getItemApi(b.build_id);
                        const nm = (api && api.name) || '';
                        if (nm.indexOf('箱') >= 0 || nm.toLowerCase().indexOf('box') >= 0) boxes.push(b);
                    }
                }
            }
            if (!boxes.length) { onceIn(8, 'box0', function () { log('自动宝箱: 本场景无未开宝箱'); }); return; }
            log('自动宝箱: ' + boxes.length + ' 个');
            boxes.forEach(function (b, i) {
                setTimeout(function () {
                    try { mo.openTreasure(b); } catch (e) { }
                }, i * 500);
            });
        });
        // 自动钓鱼:必须玩家已走到水边(用面向格子 currFaceTile),自动 抛竿->咬钩提竿;顺带收渔网
        const netLoop = mkAutoLoop('net', '自动钓鱼', function () {
            const mm = MM();
            const sc = mm.INFO.getCurrScene();
            if (!sc) return;
            const fd = sc.fishMain && sc.fishMain.getFishData && sc.fishMain.getFishData();
            if (fd) {
                const face = (sc.CURDATA && sc.CURDATA.currFaceTile) || (mm.ISLANDDATA && mm.ISLANDDATA.currFaceTile) || null;
                const mp = sc.CURDATA && sc.CURDATA.sceneTileData && sc.CURDATA.sceneTileData.map_pool;
                function isPoolTile(t) {
                    if (!t || !mp || !mp.getPoolIds) return false;
                    try {
                        const ids = mp.getPoolIds();
                        for (const k in ids) {
                            const arr = ids[k];
                            if (!arr || !arr.length) continue;
                            for (const it of arr) {
                                const s = (typeof it === 'string') ? it : (it.x + '|' + it.y);
                                if (s === (t.x + '|' + t.y)) return true;
                            }
                        }
                    } catch (e) { }
                    return false;
                }
                if (!fd.FISHING) {
                    FISH_ST.since = 0; FISH_ST.get = 0;
                    if (isPoolTile(face)) {
                        log('钓鱼: 抛竿 @' + face.x + '|' + face.y);
                        try { mm.MAPOPERATION.deal_pool(face); } catch (e) { }
                        FISH_ST.since = Date.now();
                    } else {
                        onceIn(10, 'fishhint', function () { log('钓鱼: 请先走到水边面向水面(我方不乱走位),到位后自动抛竿'); });
                    }
                } else if (mm.SCENEDATA.IF_FISHGET || fd.getFIshNode) {
                    if (!FISH_ST.get) FISH_ST.get = Date.now();
                    log('钓鱼: 提竿!');
                    try { mm.MAPOPERATION.deal_pool(face || null, true, function () { }); } catch (e) { }
                    if (Date.now() - FISH_ST.get > 6000) {
                        log('钓鱼: 提竿无效,强制结算');
                        try { mm.MAPOPERATION.deal_pool(null, true, function () { }); } catch (e) { }
                        FISH_ST.get = 0;
                    }
                } else {
                    FISH_ST.get = 0;
                    if (!FISH_ST.since) FISH_ST.since = Date.now();
                    if (Date.now() - FISH_ST.since > 15000) {
                        log('钓鱼: 久不咬钩,强制收竿重抛');
                        try { mm.MAPOPERATION.deal_pool(null, true, function () { }); } catch (e) { }
                        FISH_ST.since = Date.now();
                    }
                }
            }
            // 顺带收渔网
            const bs = allBuildings().filter(function (b) { return isApiMatch(b, ['网', 'fishnet']); });
            bs.forEach(function (b, i) {
                setTimeout(function () {
                    try { mm.MAPOPERATION.deal_fishingnet({ id: b.id, locate: b.locate }); } catch (e) { }
                }, i * 500);
            });
        });
        // 自动捕捉:复刻游戏抓捕回调逻辑(deal_animal 的 ifgacha 对话框内代码)
        //   入库 addCapturedAnimalBag + 移除野怪节点 + 清 wildAnimals
        const catchLoop = mkAutoLoop('catch', '自动捕捉', function () {
            const mm = MM();
            const sc = mm.INFO.getCurrScene();
            const am = sc && sc.animalManager;
            if (!am || !am.getAnimalData) return;
            const ad = am.getAnimalData();
            const orgin = am.orginData || ad;
            const targets = [];
            for (const k in ad) {
                const v = ad[k];
                const isWildPool = (k === 'wildAnimals' || k === 'gachaAnimal');
                if (!isWildPool) continue;
                if (Array.isArray(v)) {
                    for (const a of v) if (a && a.id !== undefined && a.animal_id !== undefined) targets.push({ a: a, pool: v });
                } else if (v && typeof v === 'object') {
                    for (const id in v) {
                        const a = v[id];
                        if (a && a.id !== undefined && a.animal_id !== undefined) targets.push({ a: a, pool: v });
                    }
                }
            }
            if (!targets.length) {
                onceIn(6, 'catch0', function () { log('自动捕捉: 无野动物目标;昆虫/飞鸟需走近它们(面向后自动捕捉)'); });
            } else {
                // 第一步:把野动物拉到角色面前(触发游戏的走近/面向可捕捉状态)
                const pnode = (function () {
                    try {
                        const mv = mm.MAINVIEW || (sc.CURVIEW || {});
                        const r = mv.role_AI || mv.role || sc.CURVIEW && (sc.CURVIEW.role_AI || sc.CURVIEW.role);
                        return r && (r.node || r);
                    } catch (e) { return null; }
                })();
                if (pnode && pnode.setPosition) {
                    for (const t of targets) {
                        if (TPED[t.a.id]) continue;
                        try {
                            const n = am.getNodeById(t.a.id);
                            if (n && cc.isValid && cc.isValid(n)) {
                                n.setPosition(pnode.x, pnode.y);
                                TPED[t.a.id] = 1;
                                log('自动捕捉: 已把动物 ' + t.a.id + ' 拉到面前');
                            }
                        } catch (e) { }
                    }
                }
            }
            // 第二步:面向的动物 currAnimal 出现后确认抓捕(复刻对话确认回调:入库+清理)
            const ca = sc.CURDATA && sc.CURDATA.currAnimal;
            if (ca && ca.id !== undefined && ca.animal_id !== undefined && !TPED['done' + ca.id]) {
                try {
                    const api = mm.ADATA.getItemApi(ca.animal_id);
                    const o = (mm.UTILS && mm.UTILS.objCopy) ? mm.UTILS.objCopy(ca) : Object.assign({}, ca);
                    o.ifgacha = 0; o.pack = 0; o.id = orgin.getAutoAnimalId ? orgin.getAutoAnimalId() : o.id; o.wild = 0;
                    const s = mm.ANIMALDATA.getAnimalBagItemId
                        ? mm.ANIMALDATA.getAnimalBagItemId(o.animal_id, !!o.blood_id)
                        : mm.ANIMALDATA.getAnimalBag(api.sub_type);
                    if (!s || mm.PACKDATA.checkPackLimit({ item_id: s, num: 1, rare: 0, ip: -1, box: 0, relate_id: 0 })) {
                        log('自动捕捉: 动物背包已满,跳过');
                    } else if (mm.GDATA.addCapturedAnimalBag(o, 'reststop')) {
                        TPED['done' + ca.id] = 1;
                        log('自动捕捉: 已捕获 ' + ((api && api.name) || ca.animal_id));
                        try { const n = am.getNodeById(ca.id); if (n) { if (n.removeFromParent) n.removeFromParent(true); if (n.destroy) n.destroy(); } } catch (e) { }
                        try { for (const key in ad) if (ad[key] && typeof ad[key] === 'object' && ad[key][ca.id] !== undefined) delete ad[key][ca.id]; } catch (e) { }
                    }
                } catch (e) { }
            }
            // 昆虫/飞鸟:走近后面向目标时 currInsect/currBird 会被置位,直接走游戏自己的捕捉流程
            const cd = (sc.CURDATA && sc.CURDATA.currInsect) || (mm.ISLANDDATA && mm.ISLANDDATA.currInsect);
            if (cd) { log('自动捕捉: 昆虫'); try { mm.MAPOPERATION.deal_insect(cd); } catch (e) { } }
            const bd = (sc.CURDATA && sc.CURDATA.currBird) || (mm.ISLANDDATA && mm.ISLANDDATA.currBird);
            if (bd) { log('自动捕捉: 飞鸟'); try { mm.MAPOPERATION.deal_bird(bd); } catch (e) { } }
        });
        document.getElementById('xsxn_autobox').onclick = function () {
            AUTO2.box.on = !AUTO2.box.on;
            this.textContent = '自动宝箱:' + (AUTO2.box.on ? '开' : '关');
            if (AUTO2.box.on) boxLoop();
        };
        document.getElementById('xsxn_autonet').onclick = function () {
            AUTO2.net.on = !AUTO2.net.on;
            this.textContent = '自动渔网:' + (AUTO2.net.on ? '开' : '关');
            if (AUTO2.net.on) netLoop();
        };
        document.getElementById('xsxn_autocatch').onclick = function () {
            AUTO2.catch.on = !AUTO2.catch.on;
            this.textContent = '自动捕捉:' + (AUTO2.catch.on ? '开' : '关');
            if (AUTO2.catch.on) catchLoop();
        };
        document.getElementById('xsxn_diag3').onclick = function () {
            try {
                const mm = MM();
                if (!mm) { log('诊断: moduleMap 未就绪'); return; }
                // 1) 建筑与配置名
                const bs = allBuildings();
                const seen = {};
                for (const b of bs) {
                    if (seen[b.build_id]) continue;
                    seen[b.build_id] = 1;
                    const api = (mm.ADATA.getItemApi(b.build_id) || {});
                    log('建筑 ' + b.build_id + ' name=' + (api.name || '?') + ' api=' + (api.api || '?') + ' sub_type=' + (api.sub_type !== undefined ? api.sub_type : '?') + ' x' + bs.filter(function (x) { return x.build_id === b.build_id; }).length);
                }
                // 2) 动物数据结构
                const sc = mm.INFO.getCurrScene && mm.INFO.getCurrScene();
                const am = sc && sc.animalManager;
                if (am && am.getAnimalData) {
                    const ad = am.getAnimalData();
                    log('动物数据 keys=' + Object.keys(ad).join(','));
                    for (const k in ad) {
                        if (Array.isArray(ad[k]) && ad[k].length) {
                            let s; try { s = JSON.stringify(ad[k][0]); } catch (e) { s = '[circular]'; }
                            log('动物 ' + k + '[' + ad[k].length + '] 样例=' + String(s).slice(0, 200));
                        } else if (ad[k] && typeof ad[k] === 'object') {
                            const ks = Object.keys(ad[k]).slice(0, 5);
                            if (ks.length) { let s; try { s = JSON.stringify(ad[k][ks[0]]); } catch (e) { s = '[circular]'; } log('动物 ' + k + '(obj) 子键=' + ks.join(',') + ' 样例=' + String(s).slice(0, 200)); }
                        }
                    }
                } else log('诊断: 无 animalManager');
                // 3) 鱼相关
                log('fishMain=' + (sc && sc.fishMain ? '有' : '无') + ' birdManager=' + (sc && sc.birdManager ? '有' : '无') + ' enemyManager=' + (sc && sc.enemyManager ? '有' : '无'));
                if (sc && sc.fishMain) log('fishMain keys=' + Object.keys(sc.fishMain).slice(0, 20).join(','));
            } catch (e) { log('诊断 err: ' + (e && e.message)); }
        };
        document.getElementById('xsxn_copylog').onclick = function () {
            const txt = LOG_LINES.join('\n');
            try { GM_setClipboard(txt); } catch (e) { }
            try {
                if (navigator.clipboard && navigator.clipboard.writeText) navigator.clipboard.writeText(txt);
            } catch (e) { }
            log('日志已复制 ' + LOG_LINES.length + ' 行');
        };

        const autoBtn = document.getElementById('xsxn_auto');
        autoBtn.onclick = function () {
            AUTO.on = !AUTO.on;
            autoBtn.textContent = '自动砍树:' + (AUTO.on ? '开' : '关');
            if (AUTO.on) autoLoop();
        };

        document.getElementById('xsxn_reap').onclick = function () { reapAll(); };
        document.getElementById('xsxn_sow').onclick = function () { sowAll(); };
        document.getElementById('xsxn_dig').onclick = function () { digAll(); };
        document.getElementById('xsxn_water').onclick = function () { waterAll(true); };
        const dbgBtn = document.getElementById('xsxn_putdbg');
        dbgBtn.onclick = function () {
            const on = !PUTDBG.on;
            const n = debugPut(on);
            this.textContent = '摆放诊断:' + (on ? '开' : '关');
            log('摆放诊断 ' + (on ? '开启' : '关闭') + ',包装 ' + n + ' 个函数,现在去放一次建筑,看日志');
        };
        const farmBtn = document.getElementById('xsxn_farmauto');
        farmBtn.onclick = function () {
            FARM.on = !FARM.on;
            farmBtn.textContent = '自动农场:' + (FARM.on ? '开' : '关');
            if (FARM.on) farmLoop();
        };

        document.getElementById('xsxn_god').onclick = function () {
            try {
                GOD.on = !GOD.on;
                const hit = setNoDamage(GOD.on);
                this.textContent = '无敌:' + (GOD.on ? '开' : '关');
                log('无敌 ' + (GOD.on ? '开启' : '关闭') + ',补丁 ' + hit + ' 处' + (hit ? '' : '(进战斗场景后再试)'));
            } catch (e) {
                log('无敌 err: ' + (e && e.stack ? e.stack.split('\n').slice(0, 3).join(' | ') : e.message));
            }
        };

        document.getElementById('xsxn_dmg').onclick = function () {
            try {
                DMG.on = !DMG.on;
                const hit = setBigDamage(DMG.on);
                this.textContent = '伤害99999:' + (DMG.on ? '开' : '关');
                log('伤害99999 ' + (DMG.on ? '开启' : '关闭') + ',补丁 ' + hit + ' 处' + (hit ? '' : '(进战斗场景后再试)'));
            } catch (e) {
                log('伤害99999 err: ' + (e && e.stack ? e.stack.split('\n').slice(0, 3).join(' | ') : e.message));
            }
        };

        document.getElementById('xsxn_aoe').onclick = function () {
            AOE.on = !AOE.on;
            this.textContent = '全屏攻击:' + (AOE.on ? '开' : '关');
            if (AOE.on) { log('全屏攻击 开启(每 400ms 对全屏敌人结算一次伤害)'); aoeLoop(); }
            else log('全屏攻击 关闭');
        };

        document.getElementById('xsxn_fast').onclick = function () {
            try {
                FAST.on = !FAST.on;
                const hit = setFastAtk(FAST.on);
                this.textContent = '高速攻击:' + (FAST.on ? '开' : '关');
                log('高速攻击 ' + (FAST.on ? '开启' : '关闭') + ',补丁 ' + hit + ' 处' + (hit ? '' : '(进战斗场景后再试)'));
            } catch (e) {
                log('高速攻击 err: ' + (e && e.stack ? e.stack.split('\n').slice(0, 3).join(' | ') : e.message));
            }
        };

        document.getElementById('xsxn_terr').onclick = function () {
            try {
                TERR.on = !TERR.on;
                const hit = setNoTerrain(TERR.on);
                this.textContent = '无视地形:' + (TERR.on ? '开' : '关');
                const detail = TERR.lastNames && TERR.lastNames.length ? ' [' + TERR.lastNames.join(',') + ']' : '';
                log('无视地形 ' + (TERR.on ? '开启' : '关闭') + ',补丁 ' + hit + ' 处' + detail + (hit ? '' : '(进主场景/摆放状态后再试)'));
            } catch (e) {
                log('无视地形 err: ' + (e && e.stack ? e.stack.split('\n').slice(0, 3).join(' | ') : e.message));
            }
        };

        // 拖动(避开按钮/日志区域,仅标题条触发)
        const dragBar = p.querySelector('#xsxn_drag');
        let sx = 0, sy = 0, sl = 0, st = 0, dragging = false;
        dragBar.addEventListener('pointerdown', function (e) {
            dragging = true;
            const r = p.getBoundingClientRect();
            sl = r.left; st = r.top;
            sx = e.clientX; sy = e.clientY;
            p.style.left = sl + 'px'; p.style.top = st + 'px';
            p.style.right = 'auto'; p.style.bottom = 'auto';
            dragBar.setPointerCapture(e.pointerId);
            e.preventDefault();
        });
        dragBar.addEventListener('pointermove', function (e) {
            if (!dragging) return;
            p.style.left = Math.max(0, Math.min(window.innerWidth - 60, sl + e.clientX - sx)) + 'px';
            p.style.top = Math.max(0, Math.min(window.innerHeight - 30, st + e.clientY - sy)) + 'px';
        });
        dragBar.addEventListener('pointerup', function () { dragging = false; });
    }
    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', addUI);
    else addUI();

    // ============================================================
    // 第三部分:资源扫描与砍树(调用游戏自身 deal_chores)
    // ============================================================
    const AUTO = { on: false };

    function MM() { const app = getApp(); return app && app.moduleMap; }

    function listChores() {
        const mm = MM();
        if (!mm) { log('moduleMap 未就绪'); return []; }
        const out = [];
        try {
            // 路径1: INFO.getCurrScene().CURDATA.sceneBuildData.getBuildingData()
            const sc = mm.INFO && mm.INFO.getCurrScene && mm.INFO.getCurrScene();
            if (!sc) { log('getCurrScene 无场景'); }
            else {
                const cd = sc.CURDATA;
                if (!out.length) log('场景=' + (sc.constructor && sc.constructor.name || '?') +
                    ' CURDATA=' + (cd ? Object.keys(cd).join(',') : '无'));
                const bd = cd && cd.sceneBuildData;
                const data = bd && bd.getBuildingData && bd.getBuildingData();
                if (data) {
                    for (const id in data) {
                        const b = data[id];
                        if (!b || !b.build_id) continue;
                        const cfg = mm.ADATA.API_CHORES[b.build_id];
                        if (!cfg) continue;
                        const isTree = !!(mm.ADATA.API_TREE && mm.ADATA.API_TREE[b.build_id]);
                        out.push({
                            id: +id, build_id: b.build_id, hp: b.hp, locate: b.locate,
                            kind: isTree ? 'tree' : (cfg.api || 'chore'),
                            tools: cfg.tools, name: cfg.name || '',
                        });
                    }
                }
                // 路径2: sceneTileData.build_datas["x|y"] = [建筑数组]
                if (!out.length && cd && cd.sceneTileData && cd.sceneTileData.build_datas) {
                    const tds = cd.sceneTileData.build_datas;
                    for (const k in tds) {
                        const arr = tds[k];
                        if (!Array.isArray(arr)) continue;
                        for (const b of arr) {
                            if (!b || !b.build_id) continue;
                            const cfg = mm.ADATA.API_CHORES[b.build_id];
                            if (!cfg) continue;
                            const isTree = !!(mm.ADATA.API_TREE && mm.ADATA.API_TREE[b.build_id]);
                            out.push({
                                id: b.id, build_id: b.build_id, hp: b.hp, locate: k,
                                kind: isTree ? 'tree' : (cfg.api || 'chore'),
                                tools: cfg.tools, name: cfg.name || '',
                            });
                        }
                    }
                }
            }
            // 路径3: 全 moduleMap 扫描含 getBuildingData 的模块(终极兜底)
            if (!out.length) {
                let hit = [];
                for (const key in mm) {
                    const mod = mm[key];
                    if (mod && typeof mod.getBuildingData === 'function') {
                        try {
                            const d = mod.getBuildingData();
                            const n = d ? Object.keys(d).length : 0;
                            hit.push(key + '(' + n + ')');
                            if (n && !out.length) {
                                for (const id in d) {
                                    const b = d[id];
                                    if (b && b.build_id && mm.ADATA.API_CHORES[b.build_id]) {
                                        const cfg = mm.ADATA.API_CHORES[b.build_id];
                                        const isTree = !!(mm.ADATA.API_TREE && mm.ADATA.API_TREE[b.build_id]);
                                        out.push({ id: +id, build_id: b.build_id, hp: b.hp, locate: b.locate, kind: isTree ? 'tree' : (cfg.api || 'chore'), tools: cfg.tools, name: cfg.name || '' });
                                    }
                                }
                            }
                        } catch (e) { }
                    }
                }
                if (hit.length) log('兜底扫描 getBuildingData 模块: ' + hit.join(' '));
                else log('所有模块均无 getBuildingData(可能在山体/特殊场景)');
            }
        } catch (e) { log('scan err: ' + e.message); }
        return out;
    }

    // kind: 'tree' | 'mine' | null(全部)
    // mine 的判定:非 tree 且 tools 不是斧头的杂役建筑(石头/矿石)
    let CHOP_RUNNING = false;
    function chopAll(kind) {
        if (CHOP_RUNNING) { log('上一轮还在跑'); return; }
        const list = listChores();
        const targets = list.filter(function (b) {
            if (kind === 'tree') return b.kind === 'tree';
            if (kind === 'mine') return b.kind !== 'tree';
            return true;
        });
        log((kind || '全部') + ' 目标 ' + targets.length + '/' + list.length);
        if (!targets.length) return;
        CHOP_RUNNING = true;
        const mm = MM();
        let i = 0, done = 0, fail = 0;
        (function next() {
            if (i >= targets.length) {
                CHOP_RUNNING = false;
                log('完成: 成功' + done + ' 失败' + fail);
                return;
            }
            const b = targets[i++];
            try {
                const parts = String(b.locate || '').split('|');
                const tile = { x: +parts[0], y: +parts[1] };
                if (isNaN(tile.x)) { fail++; log('跳过 #' + b.id + ' locate=' + b.locate); return next(); }
                log('砍 #' + b.id + ' build=' + b.build_id + ' hp=' + b.hp + ' @' + b.locate);
                mm.MAPOPERATION.deal_chores(tile, { first: true, multi_chore: true, check_permi: false }, function () {
                    done++;
                });
            } catch (e) {
                fail++; log('err #' + b.id + ': ' + e.message);
            }
            setTimeout(next, 500);
        })();
    }

    function autoLoop() {
        if (!AUTO.on) return;
        try {
            const list = listChores();
            if (list.length) {
                log('自动模式: 剩余资源 ' + list.length);
                chopAll(null);
            }
        } catch (e) { log('auto err: ' + e.message); }
        setTimeout(autoLoop, 5000);
    }

    // 调试快捷方式
    const __xsxn = {
        app: function () { return getApp(); },
        mm: function () { return MM(); },
        scan: listChores,
        chop: chopAll,
        farm: { scan: scanFarm, reap: reapAll, sow: sowAll, dig: digAll, water: waterAll },
        terr: function (on) { TERR.on = on === undefined ? !TERR.on : !!on; const hit = setNoTerrain(TERR.on); log('无视地形 ' + TERR.on + ' 补丁' + hit + '处 [' + (TERR.lastNames || []).join(',') + ']'); return hit; },
        god: function (on) { GOD.on = on === undefined ? !GOD.on : !!on; const hit = setNoDamage(GOD.on); log('无敌 ' + GOD.on + ' 补丁' + hit + '处'); return hit; },
        dmg: function (on) { DMG.on = on === undefined ? !DMG.on : !!on; const hit = setBigDamage(DMG.on); log('伤害99999 ' + DMG.on + ' 补丁' + hit + '处'); return hit; },
        aoe: function (on) { AOE.on = on === undefined ? !AOE.on : !!on; if (AOE.on) aoeLoop(); log('全屏攻击 ' + AOE.on); return AOE.on; },
        fast: function (on) { FAST.on = on === undefined ? !FAST.on : !!on; const hit = setFastAtk(FAST.on); log('高速攻击 ' + FAST.on + ' 补丁' + hit + '处'); return hit; },
        diag: function () {
            const mm = MM();
            if (!mm) { log('diag: 无 moduleMap'); return; }
            const sc = mm.INFO && mm.INFO.getCurrScene && mm.INFO.getCurrScene();
            if (!sc) { log('diag: 无场景'); return; }
            log('diag 场景=' + (sc.constructor && sc.constructor.name || '?') + ' keys=' + Object.keys(sc).slice(0, 25).join(','));
            const em = sc.enemyManager;
            log('diag enemyManager=' + (em ? '有 keys=' + Object.keys(em).slice(0, 15).join(',') : '无'));
            let nodes = null;
            try { nodes = em && em.getAllEnemyNode && em.getAllEnemyNode(); } catch (e) { log('diag getAllEnemyNode err: ' + e.message); }
            log('diag 敌人节点=' + (nodes ? nodes.length : 'getAllEnemyNode 不存在'));
            if (nodes && nodes.length) {
                const nd = nodes[0];
                const comps = (nd._components || []).map(function (c) { return c && c.constructor && c.constructor.name; });
                log('diag 敌人组件=' + comps.join(','));
                for (const c of nd._components || []) {
                    if (c && typeof c.dealDamage === 'function') {
                        log('diag dealDamage@' + (c.constructor && c.constructor.name || '?') + ' src=' + Function.prototype.toString.call(c.dealDamage).slice(0, 150));
                    }
                }
            }
            const rv = sc.CURVIEW && sc.CURVIEW.role_ATK;
            log('diag role_ATK=' + (rv ? (rv.constructor && rv.constructor.name || '?') : '无'));
            if (rv) {
                log('diag role_ATK keys=' + Object.keys(rv).slice(0, 25).join(','));
                let p = Object.getPrototypeOf(rv), depth = 0;
                while (p && depth++ < 6) {
                    if (Object.getOwnPropertyDescriptor(p, 'robotAutoAttack')) { log('diag robotAutoAttack 在原型链第' + depth + '层'); break; }
                    p = Object.getPrototypeOf(p);
                }
                if (!p) log('diag robotAutoAttack 未找到(原型链 6 层内)');
            }
            // 深扫 moduleMap 找 dealDamage / robotAutoAttack 挂载点
            const hits = [], seen = new Set();
            function scan(obj, path, depth) {
                if (!obj || depth > 3) return;
                if (typeof obj === 'function') {
                    try {
                        if (obj.prototype && !seen.has(obj.prototype)) {
                            seen.add(obj.prototype);
                            for (const k of ['dealDamage', 'robotAutoAttack']) {
                                if (Object.getOwnPropertyDescriptor(obj.prototype, k)) hits.push(path + '.proto.' + k);
                            }
                        }
                    } catch (e) { }
                    return;
                }
                if (typeof obj !== 'object' || seen.has(obj)) return;
                seen.add(obj);
                for (const k in obj) {
                    let v; try { v = obj[k]; } catch (e) { continue; }
                    if (!v) continue;
                    const np = path + '.' + k;
                    if (k === 'dealDamage' || k === 'robotAutoAttack') hits.push(np);
                    else if (typeof v === 'object' || typeof v === 'function') scan(v, np, depth + 1);
                }
            }
            for (const key in mm) { try { scan(mm[key], key, 0); } catch (e) { } }
            log('diag moduleMap 命中: ' + (hits.slice(0, 15).join(' ') || '无'));
        },
        mem: { search: memSearch, filter: memFilter, write: memWrite, list: memList, clear: memClear },
        debugPut: function (on) { return debugPut(on); },
    };
    // 同时挂到页面真实 window:油猴沙箱的 window 在 DevTools 控制台里不可见
    try { window.__xsxn = __xsxn; } catch (e) { }
    try { PW.__xsxn = __xsxn; } catch (e) { }
    try { PW.top.__xsxn = __xsxn; } catch (e) { }

    // ============================================================
    // 摆放诊断:包装摆放链路函数,记录调用与返回值,定位拦截点
    // ============================================================
    const PUTDBG = { on: false, wrapped: [] };
    const DBG_FNS = ['normalPut', 'set_building', 'put_failed', 'put_finish', 'checkArea',
        'checkEmptyTile', 'checkOverObstruct', 'checkSpecialTile', 'checkTileUnlock',
        'checkFaceTiles', 'layOutCheck', 'checkValidTileOrFallback', 'check_can_build'];

    function debugPut(on) {
        const mm = MM();
        if (!mm) return 0;
        let n = 0;
        // 还原
        for (const w of PUTDBG.wrapped) {
            try { Object.defineProperty(w.proto, w.fn, w.desc || { value: w.orig, writable: true, configurable: true }); } catch (e) { }
        }
        PUTDBG.wrapped = [];
        if (!on) { log('摆放诊断关闭'); return 0; }
        const seen = new Set();
        for (const key in mm) {
            try {
                const mod = mm[key];
                if (!mod || typeof mod !== 'object') continue;
                const proto = Object.getPrototypeOf(mod);
                if (!proto || seen.has(proto)) continue;
                seen.add(proto);
                for (const fn of DBG_FNS) {
                    const desc = Object.getOwnPropertyDescriptor(proto, fn);
                    if (!desc || typeof desc.value !== 'function' || !desc.configurable) continue;
                    const orig = desc.value;
                    const nd = Object.assign({}, desc);
                    nd.value = function () {
                        let ret;
                        try { ret = orig.apply(this, arguments); } catch (e) { log('[PUT] ' + fn + ' throw ' + e.message); throw e; }
                        try {
                            const a0 = arguments[0];
                            const brief = a0 && typeof a0 === 'object' ? (a0.x !== undefined ? a0.x + '|' + a0.y : 'obj') : String(a0).slice(0, 20);
                            log('[PUT] ' + fn + '(' + brief + ') -> ' + ret);
                        } catch (e) { }
                        return ret;
                    };
                    Object.defineProperty(proto, fn, nd);
                    PUTDBG.wrapped.push({ proto: proto, fn: fn, orig: orig, desc: desc });
                    n++;
                }
            } catch (e) { }
        }
        log('摆放诊断开启,包装 ' + n + ' 个函数。现在尝试摆放一次建筑,然后把日志发我');
        return n;
    }

    // ============================================================
    // 第三部分半:农场(开垦 / 播种 / 收获)
    //   收获 = MAPOPERATION.reap_crop(tile)
    //   播种 = MAPOPERATION.plant_crop(tile, seedApi)
    //   开垦 = 对 FARM_RUIN(1066) 走 deal_chores(锄头杂役 -> 耕地)
    // ============================================================
    const FARM = { on: false };

    // ============================================================
    // 无视地形摆放:运行时补丁所有摆放校验函数(按钮开关,可还原)
    //   true=可放/通过: checkEmptyTile checkTileUnlock checkTileWalk judge_tile_border
    //   false=无阻挡/非特殊: checkOverObstruct checkSpecialTile checkOutBorderWalk
    // ============================================================
    const TERR = { on: false, patched: [] };
    // 最小集:只保留极性从源码确认过的;checkArea 是 set_building 的总闸(false=拦截)
    const TERR_RULES = {
        checkArea: function () { return true; },        // 摆放总闸
        checkEmptyTile: function () { return true; },   // true=空地可放
        checkTileUnlock: function () { return true; },  // true=已解锁
        checkOverObstruct: function () { return false; }, // false=无阻挡
        checkSpecialTile: function () { return false; },  // false=非特殊禁放格
        // —— 以下为 set_building 里补充确认的拦截点 ——
        check_cropLand: function () { return true; },     // 与 checkArea && 在一起,false=整个不放
        checkInUseSpecial: function () { return false; }, // true=建筑使用中拦截
        self_behave_judge: function () { return 2; },     // 移动路径要求返回 2(2!=x → return 0)
        setCheck: function () { return { state: -1 }; },  // state 2/3/4 会 toast 拦截;-1=跳过校验
    };

    function setNoTerrain(on) {
        const mm = MM();
        if (!mm) return 0;
        let hit = 0;
        const seen = new Set();
        const names = [];
        // 候补对象: 模块本身 / 模块原型 / 类导出 prototype / 模块一级嵌套对象及其原型
        function tryPatch(target) {
            if (!target || (typeof target !== 'object' && typeof target !== 'function')) return;
            if (seen.has(target)) return;
            seen.add(target);
            for (const fn in TERR_RULES) {
                const desc = Object.getOwnPropertyDescriptor(target, fn);
                if ((!desc || typeof desc.value !== 'function') && typeof target[fn] !== 'function') continue;
                if (desc && !desc.configurable) continue;
                const tag = fn + '@' + (target.constructor && target.constructor.name || '?');
                if (on) {
                    if (TERR.patched.some(function (p) { return p.tag === tag; })) { hit++; continue; }
                    TERR.patched.push({ tag: tag, target: target, fn: fn, orig: desc ? desc.value : target[fn], desc: desc });
                    try {
                        if (desc) { desc.value = TERR_RULES[fn]; Object.defineProperty(target, fn, desc); }
                        else target[fn] = TERR_RULES[fn];
                        hit++; names.push(fn);
                    } catch (e) { }
                } else {
                    const i = TERR.patched.findIndex(function (p) { return p.tag === tag; });
                    if (i >= 0) {
                        try { Object.defineProperty(target, fn, TERR.patched[i].desc || { value: TERR.patched[i].orig, writable: true, configurable: true }); } catch (e) { try { target[fn] = TERR.patched[i].orig; } catch (e2) { } }
                        TERR.patched.splice(i, 1); hit++;
                    }
                }
            }
        }
        for (const key in mm) {
            try {
                const mod = mm[key];
                tryPatch(mod);
                tryPatch(Object.getPrototypeOf(mod));
                if (mod && mod.prototype) tryPatch(mod.prototype);
                if (mod && typeof mod === 'object') {
                    for (const k2 in mod) {
                        const sub = mod[k2];
                        if (sub && (typeof sub === 'object')) {
                            tryPatch(sub);
                            tryPatch(Object.getPrototypeOf(sub));
                        }
                    }
                }
            } catch (e) { }
        }
        TERR.lastNames = names;
        return hit;
    }

    // ============================================================
    // 无敌:补丁角色受击 dealDamage(函数源码含 role_under_atk),强制伤害为 0
    //   敌人受击走 enemy_under_atk,不受影响
    // ============================================================
    const GOD = { on: false, patched: [] };

    function setNoDamage(on) {
        const mm = MM();
        if (!mm) return 0;
        let hit = 0;
        // 还原
        for (const p of GOD.patched) {
            try { Object.defineProperty(p.proto, 'dealDamage', p.desc || { value: p.orig, writable: true, configurable: true }); } catch (e) { }
        }
        GOD.patched = [];
        if (!on) return 0;
        const seen = new Set();
        function tryPatch(target) {
            if (!target || (typeof target !== 'object' && typeof target !== 'function')) return;
            if (seen.has(target)) return;
            seen.add(target);
            const desc = Object.getOwnPropertyDescriptor(target, 'dealDamage');
            if (!desc || typeof desc.value !== 'function' || !desc.configurable) return;
            const orig = desc.value;
            // 只补角色受击:函数源码里引用了 role_under_atk(敌人是 enemy_under_atk)
            let src = '';
            try { src = Function.prototype.toString.call(orig); } catch (e) { }
            if (src.indexOf('role_under_atk') < 0) return;
            const nd = Object.assign({}, desc);
            nd.value = function (t) {
                return orig.call(this, 0, ...Array.prototype.slice.call(arguments, 1));
            };
            Object.defineProperty(target, 'dealDamage', nd);
            GOD.patched.push({ proto: target, orig: orig, desc: desc });
            hit++;
        }
        for (const key in mm) {
            try {
                const mod = mm[key];
                tryPatch(mod);
                tryPatch(Object.getPrototypeOf(mod));
                if (mod && mod.prototype) tryPatch(mod.prototype);
                if (mod && typeof mod === 'object') {
                    for (const k2 in mod) {
                        const sub = mod[k2];
                        if (sub && (typeof sub === 'object' || typeof sub === 'function')) {
                            tryPatch(sub);
                            tryPatch(Object.getPrototypeOf(sub));
                            if (sub.prototype) tryPatch(sub.prototype);
                        }
                    }
                }
            } catch (e) { }
        }
        return hit;
    }

    // ============================================================
    // 伤害99999:补丁敌人受击 dealDamage(函数源码含 enemy_under_atk),伤害强制 99999
    //   角色受击走 role_under_atk,不受影响
    // ============================================================
    const DMG = { on: false, patched: [] };
    const BIG_DMG = 99999;

    function wrapDamageFn(orig) {
        return function (t) {
            return orig.call(this, BIG_DMG, ...Array.prototype.slice.call(arguments, 1));
        };
    }

    // 通用:沿原型链查找并对实例原型打一个函数补丁(marker 用于源码识别),成功返回 1
    function patchInstanceFn(proto, fnName, marker, wrapperMaker, store) {
        try {
            let p = proto, depth = 0;
            while (p && depth++ < 5) {
                const desc = Object.getOwnPropertyDescriptor(p, fnName);
                if (desc) {
                    if (typeof desc.value !== 'function' || !desc.configurable) return 0;
                    let src = '';
                    try { src = Function.prototype.toString.call(desc.value); } catch (e) { }
                    if (src.indexOf(marker) < 0) return 0;
                    const nd = Object.assign({}, desc);
                    nd.value = wrapperMaker(desc.value);
                    Object.defineProperty(p, fnName, nd);
                    store.push({ proto: p, orig: desc.value, desc: desc });
                    return 1;
                }
                p = Object.getPrototypeOf(p);
            }
            return 0;
        } catch (e) { return 0; }
    }

    // 懒加载补丁循环:
    //  主方案:直接改角色属性 ATK=99999 / ATKSPEED=0(走正常攻击管线,主场景即可生效)
    //  备用:出现敌人后按实例原型补丁 dealDamage(保险)
    const LAZY = { running: false };
    function lazyBattlePatches() {
        if (!DMG.on && !FAST.on) { LAZY.running = false; return; }
        try {
            const mm = MM();
            const sc = mm && mm.INFO && mm.INFO.getCurrScene && mm.INFO.getCurrScene();
            if (sc) {
                const rv = sc.CURVIEW && sc.CURVIEW.role_ATK;
                if (rv && rv.battle_pros) {
                    if (DMG.on && rv.battle_pros.ATK !== BIG_DMG) {
                        rv.battle_pros.ATK = BIG_DMG;
                        log('伤害99999:角色 ATK 已改为 ' + BIG_DMG);
                    }
                    if (FAST.on && rv.battle_pros.ATKSPEED !== 0) {
                        rv.battle_pros.ATKSPEED = 0;
                        log('高速攻击:角色 ATKSPEED 已置 0');
                    }
                }
                if (DMG.on && !DMG.patched.length) {
                    const em = sc.enemyManager;
                    const nodes = em && em.getAllEnemyNode && em.getAllEnemyNode();
                    if (nodes) {
                        let done = false;
                        for (const nd of nodes) {
                            const comps = (nd && nd._components) || [];
                            for (const comp of comps) {
                                if (!comp) continue;
                                if (patchInstanceFn(Object.getPrototypeOf(comp), 'dealDamage', 'enemy_under_atk', wrapDamageFn, DMG.patched)) {
                                    log('伤害99999:已补丁敌人原型 (' + (comp.constructor && comp.constructor.name || '?') + ')');
                                    done = true; break;
                                }
                            }
                            if (done) break;
                        }
                    }
                }
            }
        } catch (e) { }
        setTimeout(lazyBattlePatches, 800);
    }
    function ensureLazy() { if (!LAZY.running) { LAZY.running = true; lazyBattlePatches(); } }

    function wrapFastFn(orig) {
        return function () {
            try { if (this.battle_pros) this.battle_pros.ATKSPEED = 0; } catch (e) { }
            return orig.apply(this, arguments);
        };
    }

    function setBigDamage(on) {
        const mm = MM();
        if (!mm) return 0;
        let hit = 0;
        // 还原
        for (const p of DMG.patched) {
            try { Object.defineProperty(p.proto, 'dealDamage', p.desc || { value: p.orig, writable: true, configurable: true }); } catch (e) { }
        }
        DMG.patched = [];
        if (!on) return 0;
        const seen = new Set();
        function tryPatch(target) {
            if (!target || (typeof target !== 'object' && typeof target !== 'function')) return;
            if (seen.has(target)) return;
            seen.add(target);
            const desc = Object.getOwnPropertyDescriptor(target, 'dealDamage');
            if (!desc || typeof desc.value !== 'function' || !desc.configurable) return;
            const orig = desc.value;
            // 只补敌人受击:函数源码里引用了 enemy_under_atk(角色是 role_under_atk)
            let src = '';
            try { src = Function.prototype.toString.call(orig); } catch (e) { }
            if (src.indexOf('enemy_under_atk') < 0) return;
            const nd = Object.assign({}, desc);
            nd.value = function (t) {
                return orig.call(this, BIG_DMG, ...Array.prototype.slice.call(arguments, 1));
            };
            Object.defineProperty(target, 'dealDamage', nd);
            DMG.patched.push({ proto: target, orig: orig, desc: desc });
            hit++;
        }
        for (const key in mm) {
            try {
                const mod = mm[key];
                tryPatch(mod);
                tryPatch(Object.getPrototypeOf(mod));
                if (mod && mod.prototype) tryPatch(mod.prototype);
                if (mod && typeof mod === 'object') {
                    for (const k2 in mod) {
                        const sub = mod[k2];
                        if (sub && (typeof sub === 'object' || typeof sub === 'function')) {
                            tryPatch(sub);
                            tryPatch(Object.getPrototypeOf(sub));
                            if (sub.prototype) tryPatch(sub.prototype);
                        }
                    }
                }
            } catch (e) { }
        }
        if (on && !hit) { log('伤害99999:moduleMap 扫描未命中,进入战斗后自动补丁'); ensureLazy(); }
        return hit;
    }

    // ============================================================
    // 全屏攻击:定时对全屏所有敌人结算一次伤害(走敌人自身 dealDamage,动画/死亡流程正常)
    // ============================================================
    const AOE = { on: false };

    function aoeLoop() {
        if (!AOE.on) return;
        try {
            const mm = MM();
            const sc = mm && mm.INFO && mm.INFO.getCurrScene && mm.INFO.getCurrScene();
            const em = sc && sc.enemyManager;
            const nodes = em && em.getAllEnemyNode && em.getAllEnemyNode();
            if (nodes && nodes.length) {
                const rv = sc.CURVIEW && sc.CURVIEW.role_ATK;
                const atk = (rv && rv.battle_pros && rv.battle_pros.ATK) || 1000;
                const dmg = DMG.on ? BIG_DMG : atk;
                for (const nd of nodes) {
                    try {
                        if (!nd || (typeof cc !== 'undefined' && cc.isValid && !cc.isValid(nd))) continue;
                        const comps = nd._components || [];
                        for (const en of comps) {
                            if (en && en.battle_pros && en.battle_pros.HP > 0 && typeof en.dealDamage === 'function') {
                                en.dealDamage(dmg, 0, 0, true);
                                break;
                            }
                        }
                    } catch (e) { }
                }
            }
        } catch (e) { }
        setTimeout(aoeLoop, 400);
    }

    // ============================================================
    // 高速攻击:补丁 robotAutoAttack,每次把攻击间隔 ATKSPEED 清零(判定 t-last= MEM_MAX_HITS) break;
                } else if (d < MEM_MAX_DEPTH && !isSkipObj(v) && !visited.has(v)) {
                    visited.add(v);
                    queue.push({ o: v, p: p + '.' + k, d: d + 1 });
                }
                nodes++;
                if (nodes >= MEM_MAX_NODES) break;
            }
        }
        log('内存搜索 ' + val + ':命中 ' + MEM.hits.length + ' 处(扫描 ' + nodes + ' 节点)');
        memList();
        return MEM.hits.length;
    }

    function memFilter(val) {
        const before = MEM.hits.length;
        MEM.hits = MEM.hits.filter(function (h) {
            try { return h.obj[h.key] === val; } catch (e) { return false; }
        });
        log('内存过滤 ' + val + ':' + before + ' -> ' + MEM.hits.length + ' 处');
        memList();
        return MEM.hits.length;
    }

    function memWrite(val) {
        let ok = 0;
        for (const h of MEM.hits) {
            try {
                const desc = Object.getOwnPropertyDescriptor(h.obj, h.key);
                if (desc && desc.get && !desc.set) continue; // 只读 getter 跳过
                h.obj[h.key] = val;
                ok++;
            } catch (e) { }
        }
        log('内存写入 ' + val + ':成功 ' + ok + '/' + MEM.hits.length + ' 处');
        return ok;
    }

    function memList() {
        for (const h of MEM.hits.slice(0, 30)) {
            let cur;
            try { cur = h.obj[h.key]; } catch (e) { cur = '?'; }
            log('  ' + h.path + ' = ' + cur);
        }
        if (MEM.hits.length > 30) log('  ... 其余 ' + (MEM.hits.length - 30) + ' 处省略');
        return MEM.hits.length;
    }

    function memClear() { MEM.hits = []; log('内存结果已清空'); }

    // 全量建筑扫描(多路径兜底),返回 [{id, build_id, locate, hp, time, regrow}]
    function allBuildings() {
        const mm = MM();
        let data = null;
        try {
            const sc = mm.INFO.getCurrScene();
            const cd = sc && sc.CURDATA;
            if (cd && cd.sceneBuildData && cd.sceneBuildData.getBuildingData) data = cd.sceneBuildData.getBuildingData();
        } catch (e) { }
        if (!data) { // 兜底:扫 moduleMap 里所有 getBuildingData
            for (const key in mm) {
                const mod = mm[key];
                if (mod && typeof mod.getBuildingData === 'function') {
                    try { const d = mod.getBuildingData(); if (d && Object.keys(d).length) { data = d; break; } } catch (e) { }
                }
            }
        }
        const out = [];
        if (data) for (const id in data) {
            const b = data[id];
            if (b && b.build_id) out.push({ id: +id, build_id: b.build_id, locate: String(b.locate || ''), hp: b.hp, time: b.time, regrow: b.regrow });
        }
        return out;
    }

    // 扫描农场: {crops, emptyLand, ruins, seeds}
    function scanFarm() {
        const mm = MM();
        if (!mm) { log('moduleMap 未就绪'); return null; }
        const out = { crops: [], emptyLand: [], ruins: [], seeds: [] };
        try {
            const bs = allBuildings();
            if (!bs.length) { log('拿不到建筑数据(场景未加载完?)'); return out; }
            const now = Date.now() / 1000;
            const landTiles = {}, cropTiles = {};
            for (const b of bs) {
                if (mm.ADATA.API_SEED[b.build_id]) { // 已种作物
                    cropTiles[b.locate] = 1;
                    out.crops.push({ tile: b.locate, id: b.id, build_id: b.build_id, mature: !b.time || b.time <= now, time: b.time });
                } else if (b.build_id === 1070) { // CROPLAND
                    landTiles[b.locate] = 1;
                } else if (b.build_id === 1066) { // FARM_RUIN 荒地
                    out.ruins.push({ tile: b.locate, id: b.id, hp: b.hp });
                }
            }
            for (const k in landTiles) if (!cropTiles[k]) out.emptyLand.push(k);
        } catch (e) { log('scanFarm err: ' + e.message); }
        // 背包种子(仅提示用,plant_crop 自己会校验库存)
        try {
            for (const sid in mm.ADATA.API_SEED) {
                let num = 0;
                try { const n = mm.GDATA.itemData.getItem(+sid); num = (typeof n === 'number') ? n : (n && n.num) || 0; } catch (e) { }
                if (num > 0) out.seeds.push({ id: +sid, num: num, name: (mm.ADATA.getItemApi(+sid) || {}).name || sid });
            }
        } catch (e) { }
        return out;
    }

    // 浇水 + 打水: 先对水井(1074) deal_chores 打水,再对所有作物 water_crop
    function waterAll(withWell) {
        const mm = MM();
        if (!mm) return;
        const f = scanFarm();
        if (!f.crops.length) { log('没有作物可浇'); return; }
        const run = function () {
            let i = 0, done = 0;
            (function next() {
                if (i >= f.crops.length) { log('浇水完成 ' + done + '/' + f.crops.length); return; }
                const k = f.crops[i++].tile;
                const p = k.split('|');
                try { if (mm.MAPOPERATION.water_crop({ x: +p[0], y: +p[1] }, true, 0)) done++; } catch (e) { }
                setTimeout(next, 250);
            })();
        };
        if (withWell) {
            let well = null;
            try { for (const b of allBuildings()) if (b.build_id === 1074) { well = b.locate; break; } } catch (e) { }
            if (well) {
                const wp = well.split('|');
                log('打水 @' + well);
                try {
                    // 游戏自身打水流程:deal_water(井坐标, true, 回调),自动把水桶补满
                    mm.MAPOPERATION.deal_water({ x: +wp[0], y: +wp[1] }, true, function () { });
                } catch (e) { log('打水 err: ' + e.message); }
                setTimeout(run, 2500); // 等打水动画/补满完成
                return;
            }
            log('附近没有水井(1074),直接浇水');
        }
        run();
    }

    // 收获全部成熟作物
    let REAP_RUNNING = false;
    function reapAll() {
        if (REAP_RUNNING) return;
        const f = scanFarm();
        const ripe = (f.crops || []).filter(function (c) { return c.mature; });
        log('成熟作物 ' + ripe.length + '/' + (f.crops || []).length);
        if (!ripe.length) return 0;
        REAP_RUNNING = true;
        const mm = MM();
        let i = 0, done = 0;
        (function next() {
            if (i >= ripe.length) { REAP_RUNNING = false; log('收获完成 ' + done); return; }
            const c = ripe[i++];
            try {
                const p = c.tile.split('|');
                mm.MAPOPERATION.reap_crop({ x: +p[0], y: +p[1] });
                done++;
            } catch (e) { log('reap err @' + c.tile + ': ' + e.message); }
            setTimeout(next, 400);
        })();
        return ripe.length;
    }

    // 播种: 空耕地 + 背包第一个有库存的种子
    let SOW_RUNNING = false;
    function sowAll(seedId) {
        if (SOW_RUNNING) return;
        const f = scanFarm();
        const seeds = f.seeds || [];
        if (!seeds.length) { log('背包没有种子'); return 0; }
        const seed = seedId ? seeds.find(function (s) { return s.id === seedId; }) : seeds[0];
        if (!seed) { log('找不到种子 ' + seedId); return 0; }
        const mm = MM();
        const seedApi = mm.ADATA.getItemApi(seed.id) || mm.ADATA.API_SEED[seed.id];
        log('播种: ' + (seed.name || seed.id) + ' x' + seed.num + ' -> 空地 ' + f.emptyLand.length);
        if (!f.emptyLand.length) return 0;
        SOW_RUNNING = true;
        let i = 0, done = 0;
        (function next() {
            if (i >= f.emptyLand.length || done >= seed.num) {
                SOW_RUNNING = false;
                log('播种完成 ' + done);
                if (done) setTimeout(function () { waterAll(true); }, 800); // 播种后自动浇水打水
                return;
            }
            const k = f.emptyLand[i++];
            try {
                const p = k.split('|');
                if (mm.MAPOPERATION.plant_crop({ x: +p[0], y: +p[1] }, seedApi, false)) done++;
            } catch (e) { log('sow err @' + k + ': ' + e.message); }
            setTimeout(next, 400);
        })();
        return f.emptyLand.length;
    }

    // 开垦: 缺多少开多少 = 种子总数 - 现有地块(耕地+已种),直接在空位创建 CROPLAND(1070)
    let DIG_RUNNING = false;
    function digAll(maxN) {
        if (DIG_RUNNING) return 0;
        const mm = MM();
        const f = scanFarm();
        const seedTotal = f.seeds.reduce(function (s, x) { return s + x.num; }, 0);
        const landCount = f.crops.length + f.emptyLand.length;
        let need = (maxN !== undefined) ? maxN : (seedTotal - landCount);
        if (need <= 0) { log('地够用: 地块' + landCount + ' >= 种子' + seedTotal); return 0; }
        // 已有耕地/作物位置作为锚点,螺旋找空位
        const anchors = [];
        for (const c of f.crops) anchors.push(c.tile);
        try {
            const bs = allBuildings();
            for (const b of bs) if (b.build_id === 1070) anchors.push(b.locate);
        } catch (e) { }
        if (!anchors.length) { log('没有锚点:先在农场里种一块地'); return 0; }
        const occ = {};
        try { for (const b of allBuildings()) if (b.locate) occ[b.locate] = 1; } catch (e) { }
        // 收集锚点周围空位
        const free = [];
        for (const a of anchors) {
            const p = a.split('|'), cx = +p[0], cy = +p[1];
            for (let dx = -1; dx <= 1; dx++) for (let dy = -1; dy <= 1; dy++) {
                if (!dx && !dy) continue;
                const k = (cx + dx) + '|' + (cy + dy);
                if (!occ[k]) { free.push(k); occ[k] = 1; }
            }
        }
        log('开垦: 种子' + seedTotal + ' 地块' + landCount + ' -> 开 ' + Math.min(need, free.length));
        if (!free.length) return 0;
        DIG_RUNNING = true;
        const sc = mm.INFO.getCurrScene();
        const cd = sc && sc.CURDATA;
        const bd = cd && cd.sceneBuildData;
        const td = cd && cd.sceneTileData;
        let i = 0, done = 0;
        (function next() {
            if (i >= free.length || done >= need || !bd) { DIG_RUNNING = false; log('开垦完成 ' + done); return; }
            const k = free[i++];
            try {
                const p = k.split('|');
                const o = bd.createBuildingData(1070, true); // CROPLAND
                o.locate = k;
                bd.setBuildingData(o.id, o);
                if (td && td.setTileBuilding) td.setTileBuilding(o.locate, o);
                if (bd.cloud_save) bd.cloud_save();
                try {
                    const ms = mm.INFO.mainscene;
                    if (ms && ms.buildUIManager && ms.buildUIManager.create_node_by_data) ms.buildUIManager.create_node_by_data(o);
                } catch (e) { }
                done++;
            } catch (e) { log('dig err @' + k + ': ' + e.message); }
            setTimeout(next, 300);
        })();
        return free.length;
    }

    // 自动农场循环: 收获 -> 播种 -> (间隔回合)开垦,每轮只打一条摘要
    let farmTick = 0;
    function farmLoop() {
        if (!FARM.on) return;
        farmTick++;
        try {
            const f = scanFarm();
            if (f) {
                const ripe = f.crops.filter(function (c) { return c.mature; }).length;
                const msg = '[' + farmTick + '] 作物' + f.crops.length + '(熟' + ripe + ') 空地' + f.emptyLand.length +
                    ' 荒地' + f.ruins.length + ' 种子' + f.seeds.map(function (s) { return (s.name || s.id) + 'x' + s.num; }).join(',') ;
                log(msg);
                if (ripe) reapAll();
                if (f.seeds.length) {
                    if (f.emptyLand.length) sowAll();
                    else digAll(); // 缺多少开多少
                }
            }
        } catch (e) { log('farm err: ' + e.message); }
        setTimeout(farmLoop, 10000);
    }

    // ============================================================
    // 第四部分:抓包(WebSocket + XHR)与导出、登录参数推送
    // ============================================================
    window.__pkts = [];
    const MAX = 20000;

    function logB(dir, data) {
        if (window.__pkts.length >= MAX) return;
        try {
            const b = new Uint8Array(data);
            let s = '';
            for (let i = 0; i < b.length; i++) s += String.fromCharCode(b[i]);
            window.__pkts.push({ dir: dir, t: Date.now(), len: b.length, b64: btoa(s) });
        } catch (e) { }
    }
    function logS(dir, s) {
        if (window.__pkts.length < MAX) window.__pkts.push({ dir: dir, t: Date.now(), s: String(s).slice(0, 4000) });
    }

    const OrigWS = window.WebSocket;
    function HookedWS(url, protocols) {
        const ws = protocols !== undefined ? new OrigWS(url, protocols) : new OrigWS(url);
        window.__pkts.push({ dir: 'OPEN', t: Date.now(), url: String(url) });
        ws.addEventListener('message', function (ev) {
            (ev.data instanceof ArrayBuffer) ? logB('R', ev.data) : logS('R', ev.data);
        });
        const origSend = ws.send.bind(ws);
        ws.send = function (d) {
            (d instanceof ArrayBuffer) ? logB('T', d) : logS('T', d);
            return origSend(d);
        };
        return ws;
    }
    HookedWS.prototype = OrigWS.prototype;
    HookedWS.CONNECTING = 0; HookedWS.OPEN = 1; HookedWS.CLOSING = 2; HookedWS.CLOSED = 3;
    Object.defineProperty(HookedWS, 'name', { value: 'WebSocket' });
    window.WebSocket = HookedWS;

    const oo = XMLHttpRequest.prototype.open, osn = XMLHttpRequest.prototype.send;
    XMLHttpRequest.prototype.open = function (m, u) { this.__u = u; return oo.apply(this, arguments); };
    XMLHttpRequest.prototype.send = function (b) {
        const x = this;
        if (String(x.__u || '').search(/report|stat|hm\.baidu|cnzz|talkingdata|gif|assets\/|\.bin|\.png|\.jpg|\.mp3|\.plist|\.json$/i) < 0
            && String(x.__u || '').indexOf('xdyouxi.net') >= 0) {
            if (b) (typeof b === 'string') ? logS('T ' + x.__u, b) : logB('T ' + x.__u, b);
            const ol = x.onload;
            x.onload = function (ev) {
                try {
                    const r = (x.responseType === 'arraybuffer' || x.responseType === 'blob') ? x.response : (x.responseText || x.response || '');
                    (typeof r === 'string') ? logS('R ' + x.__u, r) : (r && logB('R ' + x.__u, r));
                } catch (e) { }
                return ol ? ol.call(x, ev) : undefined;
            };
        }
        return osn.apply(this, arguments);
    };

    function pushLogin(body) {
        try {
            GM_xmlhttpRequest({
                method: 'POST',
                url: 'http://127.0.0.1:17933/pkt',
                data: JSON.stringify(body),
                headers: { 'Content-Type': 'application/json' },
                onload: function () { log('登录参数已推送本地 bot'); },
                onerror: function () { log('本地 bot 未启动(可忽略)'); },
            });
        } catch (e) { }
    }
    const _open2 = XMLHttpRequest.prototype.open;
    XMLHttpRequest.prototype.open = function (m, u) {
        const x = this;
        if (String(u).indexOf('thirdAuthLoginNew') >= 0) {
            const _ol = x.onload;
            x.onload = function (ev) {
                try { if (x.__u_body) pushLogin(x.__u_body); } catch (e) { }
                return _ol ? _ol.call(x, ev) : undefined;
            };
        }
        return _open2.apply(this, arguments);
    };
    const _send2 = XMLHttpRequest.prototype.send;
    XMLHttpRequest.prototype.send = function (b) {
        const x = this;
        if (String(x.__u || '').indexOf('thirdAuthLoginNew') >= 0 && typeof b === 'string') x.__u_body = b;
        return _send2.apply(this, arguments);
    };

    function doExport() {
        const out = JSON.stringify(window.__pkts);
        log('抓包共 ' + window.__pkts.length + ' 条');
        try { GM_setClipboard(out); log('已复制到剪贴板'); } catch (e) { }
        const blob = new Blob([out], { type: 'application/json' });
        const a = document.createElement('a');
        a.href = URL.createObjectURL(blob);
        a.download = 'xsxn_pkts_' + Date.now() + '.json';
        a.click();
    }
    GM_registerMenuCommand('导出抓包数据', doExport);
    window.__dump = doExport;

    // ============================================================
    // 第五部分:等待游戏就绪
    // ============================================================
    let readyTries = 0;
    (function waitReady() {
        const app = getApp();
        if (app && app.moduleMap && app.moduleMap.MAPOPERATION) {
            try { PW.__xsxn = window.__xsxn; } catch (e) { }
            log('游戏模块已就绪(moduleMap ' + Object.keys(app.moduleMap).length + ' 项),可点「扫资源」');
            return;
        }
        if (++readyTries > 600) { log('等待游戏模块超时(游戏可能未登录进主场景)'); return; }
        setTimeout(waitReady, 500);
    })();
})();