// ==UserScript== // @name 编程猫 Heart JIT 加速器 // @namespace https://gitee.com/shuiyu1123/codemao-webgl-engine // @version 3.1.0 // @description 绕过 Heart 运行时解释器,将积木 AST 直接编译为原生 JS 执行 // @author codemao-webgl-engine // @match https://player.codemao.cn/* // @match https://shequ.codemao.cn/* // @match https://kitten.codemao.cn/* // @match https://ide.codemao.cn/* // @run-at document-start // @grant none // @license MIT // ==/UserScript== /** * ============================================================ * 编程猫 Heart JIT 加速器 v3.1 * ============================================================ * * 从 player_v2.js 源码分析得到的注入点: * * 1. PIXI.Application 创建时挂在 canvas.__pixi_app * 2. Redux store 通过 T.a.get_store() 访问 * 3. Heart 运行时通过 Object(E.b)().get_runtime_manager() 获取 * 4. 游戏循环在 requestAnimationFrame 中运行 * 5. BCM 数据加载后通过 LOAD_BCM action 触发 * * 注入策略: * - 拦截 PIXI.Application 构造函数 → 拿到 pixi app 实例 * - 通过 pixi app 的 ticker 回调找到 Heart 运行时 * - 替换 Interpreter 的 step 方法为 JIT 编译版本 * * ============================================================ */ (function () { 'use strict'; const TAG = '🐱[Heart-JIT]'; const log = (...a) => console.log(TAG, ...a); const warn = (...a) => console.warn(TAG, ...a); // ================================================================ // 配置 // ================================================================ const CONFIG = { enabled: true, debug: false, fallbackOnError: true, profileInterval: 5000, // 性能采样间隔 }; // ================================================================ // 统计 // ================================================================ const stats = { jitted: 0, interpreted: 0, fallbacks: 0, executions: 0, compileTime: 0, avgStepTime: 0, pixiApps: 0, heartFound: false, }; // ================================================================ // JIT 编译器 // ================================================================ class JITCompiler { constructor() { this.cache = new Map(); } /** * 将一个积木过程编译为原生 JS 生成器函数 * 使用生成器是为了支持 Heart 的 yield 协程机制 */ compile(blockChain) { if (!blockChain) return null; const id = blockChain.id || 'chain_' + Math.random(); if (this.cache.has(id)) return this.cache.get(id); const t0 = performance.now(); try { const body = this.emitChain(blockChain, 0); // 生成器函数,支持 yield 协程 const fn = new Function('ctx', ` "use strict"; const { runtime, data, eventBus, ohno, entity_id, interpreter_id } = ctx; let yield_count = 0; let _break_flag = false; ${body} `); stats.compileTime += performance.now() - t0; stats.jitted++; this.cache.set(id, fn); if (CONFIG.debug) log(`JIT: compiled ${blockChain.type || 'chain'} (${id}) in ${(performance.now()-t0).toFixed(2)}ms`); return fn; } catch (e) { stats.fallbacks++; if (CONFIG.debug) warn('JIT compile failed:', e); return null; } } emitChain(block, depth) { if (!block || depth > 50) return ''; let code = this.emitBlock(block, depth); if (block.next_block) { code += '\n' + this.emitChain(block.next_block, depth); } return code; } emitBlock(block, depth) { const t = block.type; const p = block.params || {}; const children = block.child_block || []; switch (t) { // === 变量 === case 'variables_set': return `runtime.set_variable(${JSON.stringify(p.VAR)}, ${this.val(p.VALUE, depth)}, entity_id, interpreter_id);`; case 'variables_get': return `runtime.get_variable(${JSON.stringify(p.VAR)}, entity_id, interpreter_id)`; case 'change_variable': { const m = p.method || 'increase'; return `(function(){var c=runtime.get_variable(${JSON.stringify(p.valname)},entity_id,interpreter_id);var a=${this.val(p.n,depth)};runtime.set_variable(${JSON.stringify(p.valname)},${m==='increase'?'c+a':'c-a'},entity_id,interpreter_id);})()`; } // === 逻辑 === case 'logic_compare': { const op = {EQ:'==',NEQ:'!=',LT:'<',LTE:'<=',GT:'>',GTE:'>='}[p.OP] || '=='; return `(${this.val(p.A,depth)} ${op} ${this.val(p.B,depth)})`; } case 'logic_operation': return p.OP === 'AND' ? `(${this.val(p.A,depth)} && ${this.val(p.B,depth)})` : `(${this.val(p.A,depth)} || ${this.val(p.B,depth)})`; case 'logic_negate': return `(!${this.val(p.BOOL,depth)})`; case 'logic_boolean': return p.BOOL === 'TRUE' ? 'true' : 'false'; // === 数学 === case 'math_arithmetic': { const ops = {ADD:'+',MINUS:'-',MULTIPLY:'*',DIVIDE:'/'}[p.OP]; if (ops) return `(${this.val(p.A,depth)} ${ops} ${this.val(p.B,depth)})`; if (p.OP === 'POWER') return `Math.pow(${this.val(p.A,depth)},${this.val(p.B,depth)})`; return `(${this.val(p.A,depth)} + ${this.val(p.B,depth)})`; } case 'math_modulo': return `(${this.val(p.DIVIDEND,depth)} % ${this.val(p.DIVISOR,depth)})`; case 'random': return `Math.floor(${this.val(p.a,depth)} + Math.random() * (${this.val(p.b,depth)} - ${this.val(p.a,depth)} + 1))`; // === 文本 === case 'text_join': { const parts = []; for (let i = 0; p['ADD'+i] !== undefined; i++) parts.push(this.val(p['ADD'+i], depth)); return parts.length ? `("" + ${parts.join(' + ')})` : '""'; } case 'text_length': return `String(${this.val(p.VALUE,depth)}).length`; case 'text_contain': return `(String(${this.val(p.TEXT1,depth)}).indexOf(String(${this.val(p.TEXT2,depth)})) !== -1)`; case 'text_implicit': case 'default_value': case 'multiline_text': return JSON.stringify(p.TEXT || p.VALUE || ''); // === 控制流 === case 'controls_if': case 'controls_if_no_else': case 'controls_if_dropdown': { const conds = p.conditions || p.if_dropdown_conditions || []; let code = ''; for (let i = 0; i < conds.length; i++) { const cond = this.val(conds[i], depth); const body = children[i] ? this.emitChain(children[i], depth+1) : ''; code += (i === 0 ? 'if' : ' else if') + `(${cond}){${body}}`; } if (children.length > conds.length) { code += ` else {${this.emitChain(children[children.length-1], depth+1)}}`; } return code; } case 'repeat_n_times': { const times = this.val(p.TIMES, depth); const body = children[0] ? this.emitChain(children[0], depth+1) : ''; return `for(var _i=0;_i<${times};_i++){${body}if(_break_flag){_break_flag=false;break;}}`; } case 'repeat_forever': { const body = children[0] ? this.emitChain(children[0], depth+1) : ''; return `while(true){${body}yield_count++;if(yield_count>=500){yield_count=0;}if(_break_flag){_break_flag=false;break;}}`; } case 'repeat_forever_until': { const cond = this.val(p.condition, depth); const body = children[0] ? this.emitChain(children[0], depth+1) : ''; return `while(!(${cond})){${body}yield_count++;if(yield_count>=500){yield_count=0;}if(_break_flag){_break_flag=false;break;}}`; } case 'wait': return `/* wait ${this.val(p.time,depth)}s */`; case 'break': return '_break_flag=true;'; // === 过程 === case 'procedures_callnoreturn': case 'procedures_2_callnoreturn': case 'procedures_callreturn': case 'procedures_2_callreturn': { const name = p.NAME || block.procedure_name || ''; const args = Object.entries(p).filter(([k]) => k !== 'NAME').map(([k,v]) => `${k}:${this.val(v,depth)}`).join(','); return `runtime.call_procedure(${JSON.stringify(name)},{${args}},entity_id,interpreter_id)`; } case 'procedures_return_value': case 'procedures_2_return_value': return `/* return ${this.val(p.VALUE,depth)} */`; // === 广播 === case 'self_broadcast': return `runtime.broadcaster_sending_message(entity_id,${JSON.stringify(p.message)},false);`; case 'self_broadcast_and_wait': return `runtime.broadcaster_sending_message(entity_id,${JSON.stringify(p.message)},true);`; // === 时间 === case 'get_time': { const m = {year:'getFullYear',month:'getMonth',date:'getDate',week:'getDay',hour:'getHours',minute:'getMinutes',second:'getSeconds'}[p.op]; return m ? `(new Date).${m}()${p.op==='month'?'+1':''}` : '0'; } case 'get_cur_frames': return 'runtime.get_elapsed_frames()'; case 'get_timer': return 'runtime.get_timer_elapsed_s()'; case 'reset_timer': return 'runtime.reset_timer();'; case 'wait_seconds': { const secs = this.val(p.time, depth); return `/* wait ${secs}s */`; } // === 列表 === case 'lists_append': return `(function(){var l=runtime.get_variable(${JSON.stringify(p.VAR)},entity_id,interpreter_id);if(Array.isArray(l))l.push(${this.val(p.VALUE,depth)});})()`; case 'lists_get_value': return `(function(){var l=runtime.get_variable(${JSON.stringify(p.VAR)},entity_id,interpreter_id);if(!Array.isArray(l))return undefined;var i=${this.val(p.INDEX,depth)};${p.TYPE==='last'?'i=l.length;':''}i=i>0?i-1:i+l.length;return l[i];})()`; case 'lists_length': return `(function(){var l=runtime.get_variable(${JSON.stringify(p.VAR)},entity_id,interpreter_id);return Array.isArray(l)?l.length:0;})()`; case 'lists_delete': return `(function(){var l=runtime.get_variable(${JSON.stringify(p.VAR)},entity_id,interpreter_id);if(!Array.isArray(l))return;var i=${this.val(p.INDEX,depth)};${p.TYPE==='last'?'l.pop();':'i=i>0?i-1:i+l.length;l.splice(i,1);'}})()`; // === 通用 domain function === default: return this.emitDomainFunc(block, depth); } } emitDomainFunc(block, depth) { const p = block.params || {}; const args = Object.entries(p).map(([k,v]) => `${k}:${this.val(v,depth)}`).join(','); return `(function(){try{var df=runtime.get_domain_function(${JSON.stringify(block.type)});if(df)df({${args}},entity_id,interpreter_id,ctx);}catch(e){if(${CONFIG.debug})console.error(${JSON.stringify(block.type)},e);}})()`; } val(v, depth) { if (v === undefined || v === null) return 'undefined'; if (typeof v === 'number' || typeof v === 'boolean') return JSON.stringify(v); if (typeof v === 'string') return JSON.stringify(v); if (typeof v === 'object' && v.type) { const inner = this.emitBlock(v, depth + 1); if (inner.length < 100 && !inner.includes(';')) return inner; return `(function(){${inner}})()`; } return JSON.stringify(v); } } // ================================================================ // 注入器 — 通过拦截 PIXI 和 RAF 找到 Heart 运行时 // ================================================================ class HeartJITInjector { constructor() { this.jit = new JITCompiler(); this.heartRM = null; // runtime_manager this.injected = false; this.patchedInterpreters = new WeakSet(); this.origRAF = window.requestAnimationFrame; this.frameCount = 0; } /** * 安装拦截器 */ install() { this.interceptRAF(); this.interceptPIXI(); this.startProfiling(); log('拦截器已安装,等待作品加载...'); } /** * 拦截 requestAnimationFrame 以检测游戏循环 */ interceptRAF() { const self = this; const orig = this.origRAF; window.requestAnimationFrame = function(cb) { return orig.call(window, function(time) { self.frameCount++; // 每 120 帧尝试一次注入 if (self.frameCount % 120 === 0 && !self.injected) { self.tryInject(); } cb(time); }); }; log('RAF 拦截已安装'); } /** * 拦截 PIXI.Application 构造函数 */ interceptPIXI() { const self = this; // 等 PIXI 加载 const check = setInterval(() => { if (window.PIXI && window.PIXI.Application) { clearInterval(check); self.hookPIXIApplication(); } }, 500); } hookPIXIApplication() { const self = this; const OrigApp = window.PIXI.Application; // 保存原始构造函数 const PatchedApp = function(...args) { const app = new OrigApp(...args); stats.pixiApps++; log(`PIXI.Application 创建 (#${stats.pixiApps})`); // 标记 canvas if (app.view) { app.view.__pixi_app = app; app.view.__heart_jit_marked = true; } // 尝试通过 ticker 找到 Heart self.tryInjectViaPIXI(app); return app; }; // 复制原型 PatchedApp.prototype = OrigApp.prototype; PatchedApp.prototype.constructor = PatchedApp; // 替换 window.PIXI.Application = PatchedApp; log('PIXI.Application 已 hook'); } /** * 通过 PIXI app 尝试注入 */ tryInjectViaPIXI(app) { // PIXI ticker 会调用 render,而 render 连接到 Heart 的更新循环 // 我们在 ticker 回调中查找 Heart 实例 const self = this; let checkCount = 0; const checkTicker = () => { checkCount++; if (self.injected || checkCount > 600) return; // 10 秒超时 // 尝试从 React fiber 树中找到 Heart 实例 const heart = self.findHeartFromDOM(); if (heart) { self.setupJIT(heart); return; } // 继续检查 requestAnimationFrame(checkTicker); }; requestAnimationFrame(checkTicker); } /** * 从 DOM 中查找 Heart 运行时实例 * 通过 React fiber 树遍历 */ findHeartFromDOM() { try { // 查找 canvas 元素 const canvas = document.querySelector('canvas'); if (!canvas) return null; // React 16+ fiber 节点 const fiberKey = Object.keys(canvas).find(k => k.startsWith('__reactFiber$') || k.startsWith('__reactInternalInstance$') ); if (!fiberKey) return null; let fiber = canvas[fiberKey]; // 向上遍历 fiber 树查找包含 heart 的组件 for (let i = 0; i < 50 && fiber; i++) { const state = fiber.memoizedState; const props = fiber.memoizedProps; // 检查 stateNode if (fiber.stateNode && fiber.stateNode.get_heart) { const heart = fiber.stateNode.get_heart(); if (heart && heart.get_runtime_manager) { return heart; } } // 检查 props 中的 heart 引用 if (props && props.get_heart) { const heart = props.get_heart(); if (heart && heart.get_runtime_manager) { return heart; } } // 检查 ref if (fiber.ref && fiber.ref.current && fiber.ref.current.get_heart) { const heart = fiber.ref.current.get_heart(); if (heart && heart.get_runtime_manager) { return heart; } } fiber = fiber.return; } // 备用:查找全局变量 if (window.__heart) return window.__heart; if (window.__pixi_app) { const app = window.__pixi_app; if (app.__heart) return app.__heart; } } catch (e) { if (CONFIG.debug) warn('DOM 查找失败:', e); } return null; } /** * 主注入尝试 */ tryInject() { if (this.injected) return true; const heart = this.findHeartFromDOM(); if (heart) { this.setupJIT(heart); return true; } // 备用方案:从 Redux store 查找 const store = this.findReduxStore(); if (store) { const state = store.getState(); // Heart 运行时可能在某个 reducer 的 state 中 if (state.cloud_states && state.cloud_states.loading_state === 'load_done') { log('检测到作品已加载,尝试从 store 获取 Heart...'); this.tryInjectFromStore(store); } } return false; } /** * 查找 Redux store */ findReduxStore() { // 编程猫用 T.a.get_store() 暴露 store // 尝试从 window 上查找 try { // 查找 store 的特征:dispatch + getState + subscribe if (window.__store) return window.__store; // 扫描 window 上的对象 for (const key of Object.keys(window)) { const obj = window[key]; if (obj && typeof obj.dispatch === 'function' && typeof obj.getState === 'function' && typeof obj.subscribe === 'function') { return obj; } } } catch (e) {} return null; } /** * 从 Redux store 尝试注入 */ tryInjectFromStore(store) { // 监听 store 变化,当 Heart 初始化完成后注入 const self = this; let lastState = store.getState(); store.subscribe(() => { const state = store.getState(); if (state.run_states && state.run_states.run_state === 1 && !self.injected) { // 游戏正在运行 self.tryInject(); } lastState = state; }); } /** * 设置 JIT 加速 */ setupJIT(heart) { if (this.injected) return; try { const rm = heart.get_runtime_manager(); if (!rm) { warn('Runtime manager 未找到'); return; } this.heartRM = rm; this.injected = true; stats.heartFound = true; // 拦截 Interpreter 创建 this.patchInterpreterFactory(rm); log('✅ Heart JIT 加速器注入成功!'); log(' Heart 运行时已找到,JIT 编译器就绪'); // 通知用户 this.showNotification('Heart JIT 加速器已激活 ⚡'); } catch (e) { warn('注入失败:', e); stats.fallbacks++; } } /** * Patch Interpreter 工厂方法 * 当新的 Interpreter 实例创建时,自动替换其 step 方法 */ patchInterpreterFactory(rm) { const self = this; const jit = this.jit; // 拦截 load 方法(当积木加载时触发) const origLoad = rm.load?.bind(rm); if (origLoad) { rm.load = function(entities) { const result = origLoad(entities); // 加载后 patch 所有 Interpreter self.patchAllInterpreters(rm); return result; }; } // 拦截 run 方法 const origRun = rm.run?.bind(rm); if (origRun) { rm.run = function() { const result = origRun(); self.patchAllInterpreters(rm); return result; }; } // 立即 patch 已有的 interpreter this.patchAllInterpreters(rm); } /** * Patch 所有活跃的 Interpreter 实例 */ patchAllInterpreters(rm) { // Heart 的 interpreter 通常存储在内部数组中 // 通过遍历 rm 的属性查找 try { for (const key of Object.keys(rm)) { const val = rm[key]; if (Array.isArray(val)) { for (const item of val) { if (item && typeof item.step === 'function' && !this.patchedInterpreters.has(item)) { this.patchInterpreter(item); } } } if (val && typeof val === 'object' && !Array.isArray(val)) { for (const subKey of Object.keys(val || {})) { const sub = val[subKey]; if (sub && typeof sub.step === 'function' && !this.patchedInterpreters.has(sub)) { this.patchInterpreter(sub); } } } } } catch (e) { if (CONFIG.debug) warn('patch interpreters 失败:', e); } } /** * Patch 单个 Interpreter 实例 */ patchInterpreter(interpreter) { if (this.patchedInterpreters.has(interpreter)) return; this.patchedInterpreters.add(interpreter); const jit = this.jit; const origStep = interpreter.step?.bind(interpreter); if (!origStep) return; // 获取根积木 const rootBlock = interpreter.root_block; if (!rootBlock) return; // JIT 编译 const jittedFn = jit.compile(rootBlock); if (!jittedFn) return; // 替换 step 方法 interpreter.step = function() { stats.executions++; try { // 尝试 JIT 执行 const ctx = { runtime: this.run_mgr || this.runtime_manager || {}, data: this.program_state || {}, eventBus: {}, ohno: {}, entity_id: this.original_entities?.target_entity || '', interpreter_id: this.original_entities?.interpreter_id || '', }; jittedFn(ctx); return { done: true }; } catch (e) { // 降级到原始解释器 stats.fallbacks++; return origStep(); } }; stats.jitted++; if (CONFIG.debug) log(`Patched interpreter: ${rootBlock.type || 'unknown'}`); } /** * 显示通知 */ showNotification(msg) { try { const div = document.createElement('div'); div.style.cssText = ` position:fixed;top:20px;left:50%;transform:translateX(-50%); background:linear-gradient(135deg,#0f0,#0a0);color:#fff; padding:12px 24px;border-radius:8px;font-family:monospace;font-size:14px; z-index:999999;box-shadow:0 4px 20px rgba(0,255,0,0.3); animation:jit-fade 3s forwards; `; div.textContent = msg; document.body.appendChild(div); // 添加动画 const style = document.createElement('style'); style.textContent = ` @keyframes jit-fade { 0% { opacity: 0; transform: translateX(-50%) translateY(-20px); } 20% { opacity: 1; transform: translateX(-50%) translateY(0); } 80% { opacity: 1; } 100% { opacity: 0; transform: translateX(-50%) translateY(-10px); } } `; document.head.appendChild(style); setTimeout(() => div.remove(), 3000); } catch (e) {} } /** * 性能监控 */ startProfiling() { setInterval(() => { if (this.injected && CONFIG.debug) { log(`Stats: jitted=${stats.jitted}, exec=${stats.executions}, fallback=${stats.fallbacks}, compile=${stats.compileTime.toFixed(1)}ms`); } }, CONFIG.profileInterval); } } // ================================================================ // 性能面板 // ================================================================ class PerfPanel { constructor() { this.el = null; this.interval = null; } show() { if (this.el) return; const panel = document.createElement('div'); panel.id = 'heart-jit-panel'; panel.style.cssText = ` position:fixed;top:10px;right:10px;z-index:999999; background:rgba(0,0,0,0.9);color:#0f0;font-family:'Courier New',monospace; font-size:11px;padding:10px 14px;border-radius:6px;border:1px solid #0f0; min-width:200px;box-shadow:0 0 15px rgba(0,255,0,0.2); cursor:move;user-select:none; `; const title = document.createElement('div'); title.style.cssText = 'font-size:13px;font-weight:bold;margin-bottom:6px;color:#0f0;border-bottom:1px solid #030;padding-bottom:4px;'; title.textContent = '🐱 Heart JIT v3.1'; panel.appendChild(title); const content = document.createElement('div'); content.id = 'jit-stats'; panel.appendChild(content); // 拖拽 let dragging = false, ox, oy; panel.addEventListener('mousedown', e => { dragging=true; ox=e.clientX-panel.offsetLeft; oy=e.clientY-panel.offsetTop; }); document.addEventListener('mousemove', e => { if(dragging){panel.style.left=(e.clientX-ox)+'px';panel.style.top=(e.clientY-oy)+'px';panel.style.right='auto';} }); document.addEventListener('mouseup', () => dragging=false); // 右键切换 debug panel.addEventListener('contextmenu', e => { e.preventDefault(); CONFIG.debug=!CONFIG.debug; }); document.body.appendChild(panel); this.el = content; this.interval = setInterval(() => this.update(), 500); } update() { if (!this.el) return; const s = stats; const status = s.heartFound ? '✅ 已注入' : '⏳ 等待...'; this.el.innerHTML = `