studio/src/editor/engine/rbxl-lua-integration.js
min cc6447b851
All checks were successful
CI / Lint (pull_request) Successful in 1m5s
CI / Build (pull_request) Successful in 2m1s
CI / Secret scan (pull_request) Successful in 27s
CI / PR size check (pull_request) Successful in 11s
CI / Deploy to S1 + S2 (pull_request) Has been skipped
fix(rbxl-import): UDim2 viewport-relative + rbxasset URL filter
- UDim2: scale теперь умножается на viewport reference (1280×720),
  раньше игнорировался — фреймы получали 0×0 и фейлились на дефолт 100×30
  или наоборот заполняли всё окно
- _udim2_pair(): пара (x,y) через _udim2_to_rublox(axis='x'|'y')
- Фильтр rbxasset:// rbxassetid:// rbxhttp:// rbxthumb:// URL'ов на пустую
  строку — браузер их не загружает, спам в console исчезает

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-08 01:49:20 +03:00

156 lines
6.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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;
}
}