studio/src/editor/engine/rbxl-lua-integration.js
min 3271e53acf feat(rbxl): уважать enabled=false из Roblox-метадаты
Roblox-скрипты с Disabled=true (например 'Clean', 'Effects' в RayGun)
это шаблоны для клонирования через :Clone(), они никогда не должны
запускаться при старте — иначе while true do wait() end в них крашит
coroutine через WASM access out of bounds.

parseRobloxLuaMeta(code) парсит JSON-метадату из второй строки
packed-кода (формат '// {"roblox_class":..., "enabled":true}').
Скрипты с enabled=false идут в rbxlSkipped, не запускаются.
2026-06-08 13:43:09 +03:00

199 lines
8.5 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 — вспомогательные функции для импорта .rbxl-карт.
*
* Старый отдельный Worker-based runtime удалён (2026-06-08): импортированные
* Lua-скрипты теперь идут через тот же LuaSharedSandbox что и user-Lua
* (см. GameRuntime.start()). Этот файл оставлен только для:
* - unpackRobloxLuaCode() — распаковка Lua из JS-комментария-обёртки;
* - handleLuaCommand() — обработка partSet/sceneCreate/sceneDelete/playerCmd
* команд от Lua-VM в BabylonScene.
*/
/** Распаковка lua_source из packed-кода. */
export function unpackRobloxLuaCode(code) {
const openTag = '/[*] lua_source:\n'.replace('[*]', '*');
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);
}
/** Парсит JSON-метадату из 2-й строки packed-кода (`// {"roblox_class":..., "enabled": true}`). */
export function parseRobloxLuaMeta(code) {
if (typeof code !== 'string') return null;
const lines = code.split('\n');
if (lines.length < 2) return null;
const metaLine = lines[1];
if (!metaLine.startsWith('// ')) return null;
try {
return JSON.parse(metaLine.slice(3));
} catch (_) {
return null;
}
}
/** Сцена → snap для 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;
}
/**
* GUI-tree для shim'а. Mapping origin → __roblox_class.
* scene.gui — массив элементов с {id, type, name, parentId, ...origin}.
* Возвращаем массив сохраняя порядок parent → child (важно для tree-сборки).
*/
export function buildLuaGuiTree(guiElements) {
if (!Array.isArray(guiElements)) return [];
const out = [];
for (const el of guiElements) {
// origin = 'roblox-textbutton' → 'TextButton'
let rblClass = 'Frame';
const origin = el.origin || '';
if (origin.startsWith('roblox-')) {
const tail = origin.slice(7);
rblClass = tail.charAt(0).toUpperCase() + tail.slice(1);
// Camel-case "textbutton" → "TextButton"
if (rblClass.toLowerCase() === 'textbutton') rblClass = 'TextButton';
else if (rblClass.toLowerCase() === 'textlabel') rblClass = 'TextLabel';
else if (rblClass.toLowerCase() === 'imagebutton') rblClass = 'ImageButton';
else if (rblClass.toLowerCase() === 'imagelabel') rblClass = 'ImageLabel';
else if (rblClass.toLowerCase() === 'textbox') rblClass = 'TextBox';
else if (rblClass.toLowerCase() === 'scrollingframe') rblClass = 'ScrollingFrame';
else if (rblClass.toLowerCase() === 'frame') rblClass = 'Frame';
} else {
// Если origin не задан — гадаем по type
const t = el.type;
if (t === 'button') rblClass = 'TextButton';
else if (t === 'text') rblClass = 'TextLabel';
else if (t === 'image') rblClass = 'ImageLabel';
else if (t === 'textbox') rblClass = 'TextBox';
}
out.push({
id: el.id,
name: el.name || rblClass,
parentId: el.parentId || null,
visible: el.visible !== false,
text: el.text || '',
__roblox_class: rblClass,
});
}
return out;
}
/**
* Обработка IPC команд от worker'а — мапим на действия в Babylon-сцене.
*/
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') {
const pm = runtime.scene3d?.primitiveManager;
if (!pm) {
console.warn('[partSet] no primitiveManager. scene3d=', !!runtime.scene3d);
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;
try {
if (typeof pm.updateInstance === 'function') pm.updateInstance(primId, patch);
else if (typeof pm.applyPatch === 'function') pm.applyPatch(primId, patch);
else if (typeof pm.update === 'function') pm.update(primId, patch);
} catch (e) {
console.error('[partSet] updateInstance failed:', e);
}
return;
}
if (cmd === 'sceneCreate') {
// Lua: Instance.new("Part") + part.Parent = workspace → создание примитива.
// payload: { primId (предложенный id), type, x, y, z, sx, sy, sz, color, anchored }
try {
const pm = runtime.scene3d?.primitiveManager;
if (!pm || typeof pm.addInstance !== 'function') return;
const opts = {
id: payload?.primId,
x: payload?.x || 0, y: payload?.y || 0, z: payload?.z || 0,
sx: payload?.sx || 1, sy: payload?.sy || 1, sz: payload?.sz || 1,
color: payload?.color,
anchored: payload?.anchored !== false,
canCollide: payload?.canCollide !== false,
};
pm.addInstance(payload?.type || 'cube', opts);
} catch (e) {
console.error('[sceneCreate]', e);
}
return;
}
if (cmd === 'sceneDelete') {
// Lua: part:Destroy() → удаление примитива.
try {
const pm = runtime.scene3d?.primitiveManager;
if (!pm || typeof pm.removeInstance !== 'function') return;
const id = payload?.primId;
if (id != null) pm.removeInstance(Number(id));
} catch (e) {
console.error('[sceneDelete]', e);
}
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;
}
if (cmd === 'guiUpdate') {
// TODO: scripts setting Visible/Text/Color on GUI → передать в GuiManager
return;
}
}