diff --git a/src/editor/engine/GameRuntime.js b/src/editor/engine/GameRuntime.js
index eb82759..a57faba 100644
--- a/src/editor/engine/GameRuntime.js
+++ b/src/editor/engine/GameRuntime.js
@@ -19,7 +19,7 @@ import { ScriptSandbox } from './ScriptSandbox';
import { STORYS_addres } from '../../api/API';
import { PhysicsWorld } from './PhysicsWorld';
import { LabelManager } from './LabelManager';
-import { startRobloxLuaScript, handleLuaCommand } from './rbxl-lua-integration.js';
+import { startRobloxLuaShared, handleLuaCommand, unpackRobloxLuaCode } from './rbxl-lua-integration.js';
export class GameRuntime {
constructor(scene3d) {
@@ -97,34 +97,13 @@ export class GameRuntime {
// (баг «стрелка-указатель не переключается на след. цель»).
let initialScene = null;
try { initialScene = this._buildSceneSnapshot(); } catch (e) { initialScene = null; }
- let rbxlStarted = 0;
- let rbxlLimited = 0;
- let rbxlFiltered = 0;
- // WebAssembly OOM при >50 Lua-VM. Лимит 50 Worker'ов + фильтрация.
- // Реальное решение — single-VM mode (TODO).
- const RBXL_LUA_LIMIT = 50;
- // Маркер пакета: первая строка "// @roblox-lua", вторая — JSON-метадата с
- // {roblox_class, enabled}. lua_source между "/* lua_source:\n" и "\n*/".
- // Размер code = маркеры (~60 байт) + lua_source. Чистый lua_source = code.length - 60.
- const RBXL_LUA_MAX_SOURCE = 2500; // больше — почти всегда серверный/admin/chat
+ // Roblox-Lua скрипты собираем для single-VM режима: один shared Worker
+ // на все скрипты, одна wasmoon-VM. Снимает WASM OOM лимит.
+ const rbxlBatch = [];
const primitives = this.projectData?.scene?.primitives || this.scene3d?.primitiveManager?.getAllData?.() || [];
for (const s of scripts) {
- // Roblox-Lua скрипты (маркер // @roblox-lua) — wasmoon Worker.
if (s && typeof s.code === 'string' && s.code.startsWith('// @roblox-lua')) {
- // Фильтр 1: только короткие скрипты (длинные = HD Admin, chat, и т.п.).
- if (s.code.length > RBXL_LUA_MAX_SOURCE + 200) { rbxlFiltered++; continue; }
- // Фильтр 2: только привязанные к Part'у (target != null) — это
- // KillBrick/Checkpoint handlers. Скрипты без target обычно сервисные.
- if (s.target == null) { rbxlFiltered++; continue; }
- if (rbxlStarted >= RBXL_LUA_LIMIT) { rbxlLimited++; continue; }
- const sb = startRobloxLuaScript(s, {
- primitives,
- onCommand: (cmd, payload) => handleLuaCommand(s.id, cmd, payload, this),
- });
- if (sb) {
- this.sandboxes.push(sb);
- rbxlStarted++;
- }
+ rbxlBatch.push(s);
continue;
}
if (!s || typeof s.code !== 'string' || !s.code.trim()) {
@@ -156,15 +135,22 @@ export class GameRuntime {
// eslint-disable-next-line no-console
console.log('[GameRuntime] sandbox started for script id=', s.id);
}
- this._log('info', `Запущено скриптов: ${this.sandboxes.length}`);
- if (rbxlStarted > 0) {
- this._log('info', `Запущено Roblox-Lua скриптов (wasmoon): ${rbxlStarted}`);
+ // Single-VM запуск всех Roblox-Lua скриптов одним shared Worker'ом.
+ let rbxlCount = 0;
+ if (rbxlBatch.length > 0) {
+ const result = startRobloxLuaShared(rbxlBatch, {
+ primitives,
+ onCommand: (cmd, payload) => handleLuaCommand(null, cmd, payload, this),
+ });
+ if (result && result.sandbox) {
+ this.sandboxes.push(result.sandbox);
+ this._rbxlSharedSandbox = result.sandbox;
+ rbxlCount = result.count;
+ }
}
- if (rbxlFiltered > 0) {
- this._log('info', `Отфильтровано Roblox-Lua скриптов (admin/chat/services): ${rbxlFiltered}`);
- }
- if (rbxlLimited > 0) {
- this._log('warn', `Пропущено ${rbxlLimited} Roblox-Lua скриптов (WASM memory limit)`);
+ this._log('info', `Запущено скриптов: ${this.sandboxes.length - (this._rbxlSharedSandbox ? 1 : 0)}`);
+ if (rbxlCount > 0) {
+ this._log('info', `Запущено Roblox-Lua скриптов (single-VM): ${rbxlCount}`);
}
// Подцепляемся на изменения HP игрока, чтобы шлать событие 'hpChange'
// во все sandbox'ы. Не перезаписываем существующий обработчик —
@@ -487,6 +473,7 @@ export class GameRuntime {
this._physicsWorld = null;
}
this.sandboxes = [];
+ this._rbxlSharedSandbox = null;
this._isRunning = false;
this._soloScriptId = null;
this._tweens = [];
diff --git a/src/editor/engine/PlayerController.js b/src/editor/engine/PlayerController.js
index 43b880e..e3a755f 100644
--- a/src/editor/engine/PlayerController.js
+++ b/src/editor/engine/PlayerController.js
@@ -702,20 +702,7 @@ export class PlayerController {
/** Загрузить GLB-модель персонажа и его анимации. */
async _loadPlayerModel() {
const source = await this._resolveModelSource();
- // DEVLOG: явно логируем какой скин и путь
- try {
- console.warn('[PlayerController.devlog] _loadPlayerModel called', JSON.stringify({
- typeId: this._modelTypeId,
- source: source ? { file: source.file, isR15: source.isR15, kind: source.kind } : null,
- active: this._active,
- manifestCount: this._skinManifest?.length || 0,
- manifestBaseUrl: this._skinManifestBaseUrl,
- }));
- } catch (e) {}
- if (!source) {
- console.error('[PlayerController.devlog] source=null, aborting');
- return;
- }
+ if (!source) return;
if (!this._active) return;
// ВАЖНО: грузим через SceneLoader напрямую, НЕ через shared-кэш
@@ -734,25 +721,15 @@ export class PlayerController {
rootUrl = source.file.substring(0, lastSlash + 1);
filename = source.file.substring(lastSlash + 1);
}
- // DEVLOG
- console.warn('[PlayerController.devlog] SceneLoader.LoadAssetContainerAsync',
- JSON.stringify({ rootUrl, filename, isDataUrl: !!source.isDataUrl }));
let container;
try {
container = await SceneLoader.LoadAssetContainerAsync(
rootUrl, filename, this.scene,
null, source.isDataUrl ? '.glb' : undefined
);
- console.warn('[PlayerController.devlog] container loaded',
- JSON.stringify({
- meshes: container?.meshes?.length || 0,
- skeletons: container?.skeletons?.length || 0,
- animations: container?.animationGroups?.length || 0,
- }));
} catch (e) {
// eslint-disable-next-line no-console
- console.error('[PlayerController.devlog] LoadAssetContainerAsync FAILED:',
- e?.message || String(e), 'url=', rootUrl + filename);
+ console.error('[PlayerController] failed to load model:', e);
return;
}
try {
diff --git a/src/editor/engine/RobloxLuaSharedSandbox.js b/src/editor/engine/RobloxLuaSharedSandbox.js
new file mode 100644
index 0000000..1c01b21
--- /dev/null
+++ b/src/editor/engine/RobloxLuaSharedSandbox.js
@@ -0,0 +1,140 @@
+/**
+ * RobloxLuaSharedSandbox — main-side менеджер ОДНОГО shared-worker'а
+ * со множеством скриптов внутри.
+ *
+ * Использование:
+ * const mgr = new RobloxLuaSharedSandbox();
+ * mgr.setOnCommand((cmd, payload) => ...);
+ * mgr.start(initialScene, worker);
+ * mgr.addScript(scriptId, targetPrimId, luaSource);
+ * ... mgr.tick(dt, sceneSnap) ...
+ * mgr.fireEvent('touched', [primId, otherInfo]);
+ * mgr.stop();
+ *
+ * GameRuntime пушит этот менеджер ОДИН РАЗ в this.sandboxes, не за каждый
+ * скрипт — поэтому в this.sandboxes теперь живёт максимум 1 RobloxLuaSharedSandbox.
+ * Совместимость с интерфейсом ScriptSandbox: те же sendXxx no-op методы.
+ */
+export class RobloxLuaSharedSandbox {
+ constructor() {
+ this.worker = null;
+ this._onCommand = null;
+ this._booted = false;
+ this._stopped = false;
+ this._scriptCount = 0;
+ this._pendingTicks = [];
+ this._pendingEvents = [];
+ this._pendingAdds = [];
+ this.scriptId = 'rbxl-shared';
+ }
+
+ setOnCommand(cb) { this._onCommand = cb; }
+
+ /** @param {Worker} worker — экземпляр (создан через `new RobloxLuaSharedWorker()` в вызывающем коде) */
+ start(initialScene, worker) {
+ if (this.worker) return;
+ this.worker = worker;
+ this.worker.onmessage = (e) => this._handle(e);
+ this.worker.onerror = (err) => {
+ this._emit('log', { level: 'error', text: `SharedWorker error: ${err.message || err}` });
+ };
+ this.worker.postMessage({
+ cmd: 'init',
+ payload: { sceneSnap: initialScene || { primitives: {} } },
+ });
+ }
+
+ /** Добавить скрипт в shared VM. */
+ addScript(scriptId, targetPrimId, luaSource) {
+ if (!this.worker) return;
+ const payload = { id: scriptId, target: targetPrimId, luaSource };
+ if (!this._booted) {
+ this._pendingAdds.push(payload);
+ return;
+ }
+ try { this.worker.postMessage({ cmd: 'addScript', payload }); } catch (e) {}
+ this._scriptCount++;
+ }
+
+ tick(dt, sceneSnap) {
+ if (!this.worker) return;
+ if (!this._booted) {
+ this._pendingTicks.push({ dt, sceneSnap });
+ return;
+ }
+ try { this.worker.postMessage({ cmd: 'tick', payload: { dt, sceneSnap } }); } catch (e) {}
+ }
+
+ fireEvent(kind, args, scriptId) {
+ if (!this.worker) return;
+ if (!this._booted) {
+ this._pendingEvents.push({ kind, args, scriptId });
+ return;
+ }
+ try { this.worker.postMessage({ cmd: 'event', payload: { kind, args, scriptId } }); } catch (e) {}
+ }
+
+ stop() {
+ this._stopped = true;
+ try { this.worker?.postMessage({ cmd: 'stop' }); } catch (e) {}
+ try { this.worker?.terminate(); } catch (e) {}
+ this.worker = null;
+ }
+
+ _handle(ev) {
+ if (this._stopped) return;
+ const { cmd, payload } = ev.data || {};
+ if (cmd === 'boot') {
+ this._booted = true;
+ for (const p of this._pendingAdds) {
+ try { this.worker.postMessage({ cmd: 'addScript', payload: p }); } catch (e) {}
+ this._scriptCount++;
+ }
+ this._pendingAdds = [];
+ for (const t of this._pendingTicks) {
+ try { this.worker.postMessage({ cmd: 'tick', payload: t }); } catch (e) {}
+ }
+ this._pendingTicks = [];
+ for (const e of this._pendingEvents) {
+ try { this.worker.postMessage({ cmd: 'event', payload: e }); } catch (e) {}
+ }
+ this._pendingEvents = [];
+ return;
+ }
+ this._emit(cmd, payload);
+ }
+
+ _emit(cmd, payload) {
+ if (this._onCommand) {
+ try { this._onCommand(cmd, payload); } catch (e) {}
+ }
+ }
+
+ // ── Совместимость с ScriptSandbox (GameRuntime ожидает эти методы) ──
+ sendSceneSnapshot(_snap) { /* tick делает то же */ }
+ sendGuiSnapshot(_snap) {}
+ sendSkinsSnapshot(_snap) {}
+ sendInventorySnapshot(_snap) {}
+ sendTerrainHeightmap(_payload) {}
+ sendGlobalEvent(payload) {
+ // GameRuntime.routeGlobalEvent шлёт {type, ...extra}.
+ if (!payload || typeof payload !== 'object') return;
+ const type = payload.type;
+ if (type === 'playerTouch' && payload.target) {
+ // target = 'primitive:' → шлём как touched на этот Part.
+ const m = /^primitive:(\d+)$/.exec(String(payload.target));
+ if (m) {
+ this.fireEvent('touched', [+m[1], { kind: 'player' }]);
+ return;
+ }
+ }
+ this.fireEvent(type || 'unknown', [payload]);
+ }
+ sendBroadcast(msg, data) { this.fireEvent('broadcast', [msg, data]); }
+ sendOnTouchEvent(payload) { this.fireEvent('touched', [payload?.primId, payload]); }
+ sendOnTickEvent(dt) { this.tick(dt, null); }
+ sendTweenDone(payload) { this.fireEvent('tweenDone', [payload]); }
+ sendSpawnResolved(payload) { this.fireEvent('spawnResolved', [payload]); }
+ setInitialSelfPosition(_p) {}
+ setModules(_modules) {}
+}
diff --git a/src/editor/engine/RobloxLuaSharedWorker.js b/src/editor/engine/RobloxLuaSharedWorker.js
new file mode 100644
index 0000000..938393c
--- /dev/null
+++ b/src/editor/engine/RobloxLuaSharedWorker.js
@@ -0,0 +1,178 @@
+/* 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;
diff --git a/src/editor/engine/rbxl-lua-integration.js b/src/editor/engine/rbxl-lua-integration.js
index bc9feff..d8258ac 100644
--- a/src/editor/engine/rbxl-lua-integration.js
+++ b/src/editor/engine/rbxl-lua-integration.js
@@ -1,26 +1,19 @@
/**
- * rbxl-lua-integration.js — обёртка для запуска Roblox-Lua скриптов
- * (импортированных через rbxl-importer) в студии.
+ * rbxl-lua-integration.js — single-VM интеграция Roblox-Lua скриптов.
*
- * Используется из GameRuntime.start: для каждого скрипта с маркером
- * `// @roblox-lua` вызываем startRobloxLuaScript(scriptObj, ctx) и оно
- * само создаёт Worker + Lua-VM (wasmoon) + Roblox API shim.
+ * Архитектура (single-VM):
+ * - Один shared Worker для ВСЕХ Roblox-Lua скриптов проекта
+ * - Один wasmoon Lua-state
+ * - Скрипты добавляются через addScript(id, target, luaSource)
*
- * ВАЖНО: импорт Worker делается через явный `?worker` синтаксис Vite —
- * это вынесено сюда чтобы изолировать от GameRuntime.js (огромный файл,
- * в нём vite-плагин analysis иногда не парсит динамические импорты).
+ * Это снимает WASM OOM (1 wasmoon ~ 16 MB, не 742 × 16 MB).
*/
-import RobloxLuaWorker from './RobloxLuaWorker.js?worker';
-import { RobloxLuaSandbox } from './RobloxLuaSandbox.js';
+import RobloxLuaSharedWorker from './RobloxLuaSharedWorker.js?worker';
+import { RobloxLuaSharedSandbox } from './RobloxLuaSharedSandbox.js';
/**
* Распаковывает Lua-исходник из поля code упакованного rbxl-importer'ом.
- * Формат поля code:
- * // @roblox-lua
- * // {"roblox_class": "Script", "enabled": true}
- * /* lua_source:
- * <ЛУА КОД>
- * *\/
+ * Формат: `// @roblox-lua\n// {meta json}\n/* lua_source:\n<код>\n*/`
*/
export function unpackRobloxLuaCode(code) {
const openTag = '/* lua_source:\n';
@@ -33,8 +26,7 @@ export function unpackRobloxLuaCode(code) {
}
/**
- * Собирает snap сцены для Lua-shim (workspace:GetChildren).
- * @param {Array} primitives — projectData.scene.primitives
+ * Snap сцены для Lua-shim (workspace:GetChildren).
*/
export function buildLuaSceneSnap(primitives) {
const out = { primitives: {} };
@@ -53,44 +45,58 @@ export function buildLuaSceneSnap(primitives) {
}
/**
- * Запускает один Roblox-Lua скрипт.
+ * Создаёт shared sandbox менеджер, добавляет все валидные скрипты и
+ * возвращает его. GameRuntime пушит результат в this.sandboxes ОДИН раз.
*
- * @param {Object} script — entry из state.scripts (id, code, target, name)
+ * @param {Array} scripts — entries из state.scripts (с маркером // @roblox-lua)
* @param {Object} ctx — { primitives, onCommand(cmd, payload) }
- * @returns {RobloxLuaSandbox|null} — sandbox для push в this.sandboxes, или null
+ * @returns {{ sandbox: RobloxLuaSharedSandbox, count: number, filtered: number } | null}
*/
-export function startRobloxLuaScript(script, ctx) {
+export function startRobloxLuaShared(scripts, ctx) {
try {
- const luaSource = unpackRobloxLuaCode(script.code);
- if (!luaSource) return null;
- const worker = new RobloxLuaWorker();
+ 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 sb = new RobloxLuaSandbox(luaSource, script.target || null);
- sb.scriptId = script.id;
- sb.setInitialScene(sceneSnap);
- sb.setOnCommand(ctx.onCommand);
- sb.start(worker);
- return sb;
+ 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] start failed for', script?.id, e?.message || e);
+ console.warn('[rbxl-lua-shared] start failed:', e?.message || e);
return null;
}
}
/**
- * Маппинг IPC команд от RobloxLuaSandbox на действия в Babylon-сцене.
+ * Маппинг IPC команд от shared sandbox на действия в Babylon-сцене.
*
- * @param {string} scriptId
+ * @param {string} _scriptId — не используется (команды от shared VM не привязаны к одному id)
* @param {string} cmd
* @param {object} payload
* @param {object} runtime — { scene3d, game }
*/
-export function handleLuaCommand(scriptId, cmd, payload, runtime) {
+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 ' + scriptId + ']', payload?.text || '');
+ fn('[rbxl-lua]', payload?.text || '');
return;
}
if (cmd === 'partSet') {
@@ -146,3 +152,9 @@ export function handleLuaCommand(scriptId, cmd, payload, runtime) {
return;
}
}
+
+/* ──────── Legacy single-script API (для обратной совместимости) ──────── */
+// Старая логика per-script Worker оставлена в RobloxLuaSandbox.js + RobloxLuaWorker.js,
+// но GameRuntime теперь использует startRobloxLuaShared. Эти экспорты не удалены
+// чтобы тесты в player/tests/ продолжали работать.
+export { startRobloxLuaShared as startRobloxLuaScript };