/** * rbxl-lua-integration.js — single-VM интеграция Roblox-Lua скриптов. * * Архитектура (single-VM): * - Один shared Worker для ВСЕХ Roblox-Lua скриптов проекта * - Один wasmoon Lua-state * - Скрипты добавляются через addScript(id, target, luaSource) * * Это снимает WASM OOM (1 wasmoon ~ 16 MB, не 742 × 16 MB). */ import RobloxLuaSharedWorker from './RobloxLuaSharedWorker.js?worker'; import { RobloxLuaSharedSandbox } from './RobloxLuaSharedSandbox.js'; /** * Распаковывает Lua-исходник из поля code упакованного rbxl-importer'ом. * Формат: "// @roblox-lua\\n// {meta json}\\n/[*] lua_source:\\n<код>\\n[*]/" */ export function unpackRobloxLuaCode(code) { const openTag = '/* lua_source:\n'; const i = code.indexOf(openTag); if (i < 0) return null; const start = i + openTag.length; const closeIdx = code.lastIndexOf('\n*/'); if (closeIdx < start) return null; return code.slice(start, closeIdx); } /** * Snap сцены для Lua-shim (workspace:GetChildren). */ export function buildLuaSceneSnap(primitives) { const out = { primitives: {} }; if (!Array.isArray(primitives)) return out; for (const p of primitives) { out.primitives[p.id] = { id: p.id, type: p.type, name: p.name, x: p.x, y: p.y, z: p.z, sx: p.sx, sy: p.sy, sz: p.sz, color: p.color, material: p.material, anchored: !!p.anchored, canCollide: p.canCollide !== false, opacity: typeof p.opacity === 'number' ? p.opacity : 1, }; } return out; } /** * Создаёт shared sandbox менеджер, добавляет все валидные скрипты и * возвращает его. GameRuntime пушит результат в this.sandboxes ОДИН раз. * * @param {Array} scripts — entries из state.scripts (с маркером // @roblox-lua) * @param {Object} ctx — { primitives, onCommand(cmd, payload) } * @returns {{ sandbox: RobloxLuaSharedSandbox, count: number, filtered: number } | null} */ export function startRobloxLuaShared(scripts, ctx) { try { const luaScripts = []; let filtered = 0; for (const s of scripts) { if (!s || typeof s.code !== 'string') continue; if (!s.code.startsWith('// @roblox-lua')) continue; const luaSource = unpackRobloxLuaCode(s.code); if (!luaSource) { filtered++; continue; } // Фильтр: скрипты декомпилированные из Synapse X / HD Admin — обычно // длинные и сервисные. Оставим только короткие с target. // Но в single-VM режиме лимита на количество нет — пробуем все. luaScripts.push({ id: s.id, target: s.target, luaSource }); } if (luaScripts.length === 0) return { sandbox: null, count: 0, filtered }; const worker = new RobloxLuaSharedWorker(); const sceneSnap = buildLuaSceneSnap(ctx.primitives); const mgr = new RobloxLuaSharedSandbox(); mgr.setOnCommand(ctx.onCommand); mgr.start(sceneSnap, worker); for (const ls of luaScripts) { mgr.addScript(ls.id, ls.target, ls.luaSource); } return { sandbox: mgr, count: luaScripts.length, filtered }; } catch (e) { // eslint-disable-next-line no-console console.warn('[rbxl-lua-shared] start failed:', e?.message || e); return null; } } /** * Маппинг IPC команд от shared sandbox на действия в Babylon-сцене. * * @param {string} _scriptId — не используется (команды от shared VM не привязаны к одному id) * @param {string} cmd * @param {object} payload * @param {object} runtime — { scene3d, game } */ export function handleLuaCommand(_scriptId, cmd, payload, runtime) { if (cmd === 'log') { const fn = payload?.level === 'error' ? console.error : payload?.level === 'warn' ? console.warn : console.log; fn('[rbxl-lua]', payload?.text || ''); return; } if (cmd === 'partSet') { try { const pm = runtime.scene3d?.primitiveManager; if (!pm) return; const primId = payload?.primId; const prop = payload?.prop; const value = payload?.value; const patch = {}; if (prop === 'position' && value) { patch.x = value.x; patch.y = value.y; patch.z = value.z; } else if (prop === 'cframe' && value) { patch.x = value.x; patch.y = value.y; patch.z = value.z; patch.rotationX = value.rx; patch.rotationY = value.ry; patch.rotationZ = value.rz; } else if (prop === 'size' && value) { patch.sx = value.sx; patch.sy = value.sy; patch.sz = value.sz; } else if (prop === 'color') patch.color = value; else if (prop === 'material') patch.material = value; else if (prop === 'anchored') patch.anchored = value; else if (prop === 'canCollide') patch.canCollide = value; else if (prop === 'opacity') patch.opacity = value; else if (prop === 'rotation' && value) { patch.rotationX = value.rx; patch.rotationY = value.ry; patch.rotationZ = value.rz; } if (typeof pm.applyPatch === 'function') pm.applyPatch(primId, patch); else if (typeof pm.update === 'function') pm.update(primId, patch); } catch (e) { /* swallow */ } return; } if (cmd === 'partVel') { try { const pm = runtime.scene3d?.primitiveManager; if (pm && typeof pm.setVelocity === 'function') { pm.setVelocity(payload.primId, payload.vx, payload.vy, payload.vz); } } catch (e) {} return; } if (cmd === 'playerCmd') { try { const p = runtime.game?.player; if (!p) return; const method = payload?.method; const args = payload?.args || []; if (method === 'teleport') p.teleport && p.teleport(args[0], args[1], args[2]); else if (method === 'setWalkSpeed') p.setWalkSpeed && p.setWalkSpeed(args[0]); else if (method === 'setJumpPower') p.setJumpPower && p.setJumpPower(args[0]); else if (method === 'setHealth') p.setHealth && p.setHealth(args[0]); else if (method === 'die') p.die && p.die(); else if (method === 'damage' || method === 'takeDamage') p.damage && p.damage(args[0]); } catch (e) {} return; } }