Some checks failed
Все 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>
179 lines
7.1 KiB
JavaScript
179 lines
7.1 KiB
JavaScript
/* 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;
|