diff --git a/src/editor/engine/lua/RobloxShim.js b/src/editor/engine/lua/RobloxShim.js index efc236f..246e050 100644 --- a/src/editor/engine/lua/RobloxShim.js +++ b/src/editor/engine/lua/RobloxShim.js @@ -25,10 +25,11 @@ const SCHEDULER = { const HEARTBEAT_SIGNAL = makeSignal(); const STEPPED_SIGNAL = makeSignal(); -// Глобальный helper для запуска Lua-handler'ов в собственной coroutine. -// Без этого Roblox-обработчики которые внутри делают wait() падают с -// "attempt to yield across a C-call boundary". -let _runHandlerInCoroutine = null; +// Очередь handler'ов которые надо запустить на следующем tickScheduler. +// Этим мы выходим из C-boundary — wait() внутри handler'а становится +// безопасным yield в собственной coroutine, потому что handler стартует +// уже из main loop, а не из синхронного JS-callback. +const _pendingHandlerQueue = []; function makeSignal() { const sig = { @@ -50,17 +51,10 @@ function makeSignal() { sig.connect = sig.Connect; sig.Fire = function (...args) { for (const fn of [...sig.connections]) { - // Запускаем handler в его собственной coroutine — это позволяет - // делать wait() внутри без yield-across-C-boundary ошибки. - if (_runHandlerInCoroutine) { - try { _runHandlerInCoroutine(fn, args); } catch (e) { - console.error('[Signal handler]', e); - } - } else { - try { fn(...args); } catch (e) { - console.error('[Signal handler]', e); - } - } + // Кладём в очередь, чтобы handler стартовал не в текущем + // JS-callback (откуда yield запрещён), а из tickScheduler + // в своей coroutine. Безопасно для wait() внутри. + _pendingHandlerQueue.push({ fn, args }); } }; sig.fire = sig.Fire; @@ -1402,11 +1396,11 @@ export function registerRobloxShim(lua, opts) { return 0 end - -- Запуск Lua-handler'а в собственной coroutine. - -- Используется при Fire сигнала из JS — иначе wait() внутри handler'а - -- падает с 'attempt to yield across a C-call boundary'. + -- Запуск Lua-handler'а из очереди в собственной coroutine. + -- Вызывается из JS tickScheduler — мы УЖЕ вышли из C-callback, + -- так что wait() внутри handler'а — yield в свою coroutine. __rbxl_next_handler_id = 0 - function __rbxl_run_in_coroutine(fn, a1, a2, a3, a4) + function __rbxl_drain_handler(fn, a1, a2, a3, a4) __rbxl_next_handler_id = __rbxl_next_handler_id + 1 local handlerId = "handler_" .. __rbxl_next_handler_id local co = coroutine.create(function() fn(a1, a2, a3, a4) end) @@ -1422,8 +1416,8 @@ export function registerRobloxShim(lua, opts) { end end `); - // Кешируем ссылку на функцию для использования из makeSignal - _runHandlerInCoroutine = lua.global.get('__rbxl_run_in_coroutine'); + // Кешируем ссылку на Lua-функцию запуска handler'а + const luaDrainHandler = lua.global.get('__rbxl_drain_handler'); // Добавим Lua-side helper для лога global.set('__log', (level, text) => { send('log', { level: String(level || 'info'), text: String(text || '') }); @@ -1469,7 +1463,20 @@ export function registerRobloxShim(lua, opts) { onDataSnapshot() {}, tickScheduler(_dt) { - // 0. Tweens + // 0a. Lua-handlers из очереди (signal.Fire отложил их сюда). + // Запускаем каждый в своей coroutine — wait() внутри безопасен. + if (_pendingHandlerQueue.length > 0) { + const queue = _pendingHandlerQueue.splice(0, _pendingHandlerQueue.length); + for (const h of queue) { + try { + const a = h.args || []; + luaDrainHandler(h.fn, a[0], a[1], a[2], a[3]); + } catch (e) { + console.error('[handler-drain]', e); + } + } + } + // 0b. Tweens _stepTweens(_dt); const now = SCHEDULER.now(); // 1. task.delay / task.defer