studio/src/editor/engine/RobloxLuaSharedWorker.js
min 624bbc636b
Some checks failed
CI / Lint (pull_request) Failing after 1m5s
CI / Build (pull_request) Failing after 49s
CI / Secret scan (pull_request) Successful in 25s
CI / PR size check (pull_request) Successful in 9s
CI / Deploy to S1 + S2 (pull_request) Has been skipped
feat(rbxl-import): single-VM, Touched, scroll-to-selected, GUI
Все 5 задач итерации:

1. Single-VM mode (RobloxLuaSharedWorker/Sandbox):
   - один Worker, одна wasmoon-VM на ВСЕ скрипты проекта
   - addScript() для каждого, общий tick/event broadcast
   - снимает WASM OOM (1 VM 16MB вместо 742 × 16MB)
   - убран per-script лимит 50, теперь все 742 загружаются

2. Touched events:
   - sendGlobalEvent в shared sandbox распознаёт playerTouch
     и пересылает в Worker как 'touched' с primId
   - Worker находит Part по __primId в workspace и Fire'ит
     его Touched сигнал — Lua-обработчики работают

3. Click в иерархии → scroll-to-selected:
   - useEffect в HierarchyPanel ловит изменение selection
     и scrollIntoView для нужного ItemRow
   - data-sel-id атрибут на primitive/model/block строках

4. GUI Roblox в конвертере:
   - ScreenGui/Frame/TextLabel/TextButton/ImageLabel/TextBox →
     scene.gui c полным набором свойств (UDim2→pixel, Color3→hex,
     BackgroundTransparency→bgOpacity, parentId)

5. Чистка:
   - удалены debug-console.warn из PlayerController._loadPlayerModel
     (убирает spam '[PlayerController.devlog]' в consoles)

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

179 lines
7.1 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.

/* eslint-disable no-restricted-globals */
/**
* RobloxLuaSharedWorker.js — ОДИН Worker, ОДНА Lua-VM, МНОЖЕСТВО скриптов.
*
* Отличие от RobloxLuaWorker (single-script-per-VM):
* - Lua-state создаётся один раз при первом `init`
* - Каждый последующий `addScript` загружает новый скрипт в ту же VM как
* отдельную функцию, вызывает её в pcall, регистрирует сигналы (Touched и т.п.)
* - Все скрипты делят:
* * один экземпляр Roblox-shim (game, workspace, script — для каждого свой)
* * один scheduler (wait/task.wait в общих корутинах)
* * один scene snapshot (workspace:GetChildren)
*
* Это снимает WASM OOM лимит: 1 wasmoon-VM ~ 16 MB, не 742 × 16.
*
* IPC (с main):
* <- init { sceneSnap }
* <- addScript { id, target, luaSource }
* <- tick { dt, sceneSnap }
* <- event { kind, args, scriptId?: id }
* <- stop
* -> boot
* -> ready { scriptId, ok, error? } — после каждого addScript
* -> log, partSet, partVel, playerCmd, broadcast — общие для всех скриптов
*/
import { LuaFactory } from 'wasmoon';
import { registerRobloxApi } from './roblox-shim.js';
const state = {
factory: null,
lua: null,
sceneSnap: { primitives: {} },
isStopped: false,
initPromise: null,
scriptIdSeq: 0,
nextSignalId: 1,
};
function send(cmd, payload) {
self.postMessage({ cmd, payload });
}
function log(level, text) {
send('log', { level, text });
}
self.addEventListener('message', async (ev) => {
const { cmd, payload } = ev.data || {};
try {
if (cmd === 'init') {
await handleInit(payload);
} else if (cmd === 'addScript') {
await handleAddScript(payload);
} else if (cmd === 'tick') {
handleTick(payload);
} else if (cmd === 'event') {
handleEvent(payload);
} else if (cmd === 'stop') {
state.isStopped = true;
try { state.lua?.global?.close?.(); } catch (e) {}
}
} catch (err) {
log('error', `SharedWorker error in ${cmd}: ${err && err.stack ? err.stack : err}`);
}
});
async function handleInit({ sceneSnap }) {
if (state.initPromise) { await state.initPromise; return; }
state.initPromise = (async () => {
state.sceneSnap = sceneSnap || { primitives: {} };
state.factory = new LuaFactory();
state.lua = await state.factory.createEngine({
injectObjects: true,
enableProxy: true,
traceAllocations: false,
});
// Регистрируем Roblox API ОДИН РАЗ для всей VM.
// `script` глобал — здесь не имеет смысла (он per-script), скрипты
// получают свой `script` через локальную таблицу при addScript.
registerRobloxApi(state.lua, {
getSceneSnap: () => state.sceneSnap,
targetPrimitiveId: null,
send,
registerSignal: () => state.nextSignalId++,
});
// Готовим helper-таблицу для скриптов
await state.lua.doString(`
-- Глобальная таблица — все скрипты регистрируют свой контекст здесь.
__rbxl_scripts = __rbxl_scripts or {}
-- helper: безопасный вызов user-функции в pcall, ошибки в warn.
function __rbxl_safe_run(id, fn)
local ok, err = pcall(fn)
if not ok then
warn("[rbxl-lua " .. tostring(id) .. " partial fail] " .. tostring(err))
end
end
`);
send('boot', null);
})();
await state.initPromise;
}
async function handleAddScript({ id, target, luaSource }) {
if (!state.lua) {
log('error', 'addScript before init');
return;
}
// Загружаем скрипт как локальную функцию которая будет вызвана в pcall.
// Создаём для него локальный script={Parent=target_part} объект через
// глобальный workspace lookup.
const safeId = String(id).replace(/[^a-zA-Z0-9_]/g, '_');
const targetExpr = target != null ? `__rbxl_lookup_part(${JSON.stringify(target)})` : 'nil';
const wrapped = `
do
local script = { Parent = ${targetExpr}, Name = "Script_${safeId}" }
__rbxl_safe_run("${safeId}", function()
${luaSource}
end)
end
`;
try {
// Регистрируем helper для lookup'а Part'а по id (один раз)
if (!state._lookupRegistered) {
await state.lua.doString(`
function __rbxl_lookup_part(id)
if not workspace or not workspace.GetChildren then return nil end
for _, c in ipairs(workspace:GetChildren()) do
if c.__primId == id then return c end
end
return nil
end
`);
state._lookupRegistered = true;
}
await state.lua.doString(wrapped);
send('ready', { scriptId: id, ok: true });
} catch (e) {
send('ready', { scriptId: id, ok: false, error: String(e?.message || e) });
}
}
function handleTick({ dt, sceneSnap }) {
if (state.isStopped || !state.lua) return;
if (sceneSnap) state.sceneSnap = sceneSnap;
// Heartbeat/Stepped/RenderStepped — через global signal'ы из shim
// (см. RunService.Heartbeat).
try {
const game = state.lua.global.get('game');
if (game && typeof game.GetService === 'function') {
const rs = game.GetService('RunService');
if (rs?.Heartbeat?.Fire) rs.Heartbeat.Fire(dt);
if (rs?.Stepped?.Fire) rs.Stepped.Fire(performance.now() / 1000, dt);
if (rs?.RenderStepped?.Fire) rs.RenderStepped.Fire(dt);
}
} catch (e) { /* swallow */ }
}
function handleEvent({ kind, args, scriptId }) {
if (state.isStopped || !state.lua) return;
// Маршрутизация событий: например 'touched' на конкретном primId.
// В MVP — пробрасываем как глобальный сигнал через RbxSignal.Fire
// на найденном Part'е (если есть в workspace).
try {
const game = state.lua.global.get('game');
const workspace = game?.Workspace;
if (kind === 'touched' && args && workspace) {
const primId = args[0];
for (const child of (workspace.Children || [])) {
if (child.__primId === primId && child.Touched?.Fire) {
child.Touched.Fire(args[1]);
}
}
}
} catch (e) { /* swallow */ }
}
self.__rbxlSharedState = state;