From 5342c079d11f00dee1656c94cfccfc912e3bb886 Mon Sep 17 00:00:00 2001 From: min Date: Mon, 8 Jun 2026 13:26:30 +0300 Subject: [PATCH] =?UTF-8?q?feat(lua):=20=D0=B2=D0=BA=D0=BB=D1=8E=D1=87?= =?UTF-8?q?=D0=B8=D0=BB=20=D0=B8=D0=BC=D0=BF=D0=BE=D1=80=D1=82=D0=B8=D1=80?= =?UTF-8?q?=D0=BE=D0=B2=D0=B0=D0=BD=D0=BD=D1=8B=D0=B5=20=D1=81=D0=BA=D1=80?= =?UTF-8?q?=D0=B8=D0=BF=D1=82=D1=8B=20+=20=D0=B7=D0=B0=D1=89=D0=B8=D1=82?= =?UTF-8?q?=D0=B0=20=D0=BE=D1=82=20=D0=BF=D0=BE=D0=B4=D0=B2=D0=B8=D1=81?= =?UTF-8?q?=D0=B0=D0=BD=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. TweenInfo.new(time, easing, direction, repeat, reverses, delay) — глобальный конструктор. Был причиной 'attempt to index nil (TweenInfo)'. 2. Расширенный Enum: InfoType, SortOrder, FillDirection, Font, TextXAlignment, ScaleType, PartType, SurfaceType, UserInputState и др. — типичные для Roblox-туториалов. 3. NumberSequence/ColorSequence/NumberRange/Rect — заглушки конструкторов. 4. _kickoff() теперь батчами по 20 скриптов через setTimeout(0). 742 скрипта инициализируются за ~37 фреймов вместо одного синхронного блока, UI не подвисает. 5. Импортированные .rbxl-скрипты ВКЛЮЧЕНЫ по умолчанию (window.__RBXL_SKIP_IMPORTED=true чтобы выключить). Падения отдельных скриптов изолированы — tickScheduler ловит ошибки и удаляет битые coroutines, остальные продолжают работать. --- src/editor/engine/GameRuntime.js | 13 +++---- src/editor/engine/lua/LuaSharedSandbox.js | 25 +++++++++++- src/editor/engine/lua/RobloxShim.js | 47 +++++++++++++++++++++++ 3 files changed, 77 insertions(+), 8 deletions(-) diff --git a/src/editor/engine/GameRuntime.js b/src/editor/engine/GameRuntime.js index 99072d9..a66c54c 100644 --- a/src/editor/engine/GameRuntime.js +++ b/src/editor/engine/GameRuntime.js @@ -121,12 +121,11 @@ export class GameRuntime { // скрипты (с маркером // @roblox-lua) теперь идут через ОДИН LuaSharedSandbox. // .rbxl-скрипты распаковываем из JS-комментария-обёртки в чистый Lua. const luaUserBatch = []; - // По умолчанию импортированные .rbxl-скрипты НЕ выполняются: - // - типичная Roblox-карта = 500-2000 скриптов, многие используют - // DataStore/Tool/PlayerGui/UserInputService, которых у нас нет; - // - даже с stub'ами сотни падений → tab подвисает; - // Юзер может включить через window.__RBXL_RUN_IMPORTED = true в консоли. - const runImportedRbxl = typeof window !== 'undefined' && window.__RBXL_RUN_IMPORTED === true; + // Импортированные .rbxl-скрипты выполняются по умолчанию (батчами по 20 + // через setTimeout, чтобы не подвешивать UI). Падения скриптов изолированы + // pcall'ом в shim — один битый скрипт не валит остальных. + // Выключить можно через window.__RBXL_SKIP_IMPORTED = true. + const runImportedRbxl = !(typeof window !== 'undefined' && window.__RBXL_SKIP_IMPORTED === true); let rbxlSkipped = 0; for (const s of scripts) { if (s && typeof s.code === 'string' && s.code.startsWith('// @roblox-lua')) { @@ -221,7 +220,7 @@ export class GameRuntime { this._log('info', `Запущено Roblox-Lua скриптов (импортированных): ${rbxlImported}`); } if (rbxlSkipped > 0) { - this._log('info', `Импортированных .rbxl-скриптов пропущено: ${rbxlSkipped}. Включить: window.__RBXL_RUN_IMPORTED = true`); + this._log('info', `Импортированных .rbxl-скриптов пропущено: ${rbxlSkipped}. Вернуть: убрать window.__RBXL_SKIP_IMPORTED`); } if (luaWritten > 0) { this._log('info', `Запущено Lua-скриптов юзера: ${luaWritten}`); diff --git a/src/editor/engine/lua/LuaSharedSandbox.js b/src/editor/engine/lua/LuaSharedSandbox.js index 12a5f53..f2943e5 100644 --- a/src/editor/engine/lua/LuaSharedSandbox.js +++ b/src/editor/engine/lua/LuaSharedSandbox.js @@ -109,10 +109,33 @@ export class LuaSharedSandbox { this._isKickedOff = true; // eslint-disable-next-line no-console console.log(`[LuaSharedSandbox] kickoff: starting ${this._pendingScripts.length} scripts`); - for (const entry of this._pendingScripts) this._startSingleScript(entry); + const pending = this._pendingScripts; this._pendingScripts = []; + // Запускаем main-loop сразу — он начнёт tick'ать как только будут coroutines. this._lastTickAt = performance.now(); this._startMainLoop(); + // Init батчами по 20 с yield между ними, чтобы UI не подвисал на 700+ скриптах. + const BATCH_SIZE = 20; + let idx = 0; + const initBatch = () => { + if (this._isStopped) return; + const end = Math.min(idx + BATCH_SIZE, pending.length); + for (let i = idx; i < end; i++) { + try { this._startSingleScript(pending[i]); } + catch (e) { + // eslint-disable-next-line no-console + console.error('[LuaSharedSandbox] init batch err:', e); + } + } + idx = end; + if (idx < pending.length) { + setTimeout(initBatch, 0); + } else { + // eslint-disable-next-line no-console + console.log(`[LuaSharedSandbox] all ${pending.length} scripts kicked off`); + } + }; + setTimeout(initBatch, 0); } _startSingleScript(entry) { diff --git a/src/editor/engine/lua/RobloxShim.js b/src/editor/engine/lua/RobloxShim.js index 9b50bdb..533f016 100644 --- a/src/editor/engine/lua/RobloxShim.js +++ b/src/editor/engine/lua/RobloxShim.js @@ -541,6 +541,53 @@ export function registerRobloxShim(lua, opts) { HumanoidStateType: mkE(['Running','Jumping','Freefall','Landed','Dead','Climbing','Swimming','Seated']), EasingStyle: mkE(['Linear','Sine','Quad','Cubic','Quart','Quint','Bounce','Elastic']), EasingDirection: mkE(['In','Out','InOut']), + // Часто используемые в туториалах + InfoType: mkE(['Asset','BundleDetails','Subscription','GamePass','UserProductsInExperience']), + SortOrder: mkE(['Name','Custom','LayoutOrder']), + FillDirection: mkE(['Horizontal','Vertical']), + HorizontalAlignment: mkE(['Left','Center','Right']), + VerticalAlignment: mkE(['Top','Center','Bottom']), + Font: mkE(['Legacy','Arial','SourceSans','Code','Highway','SciFi','Cartoon','Gotham','GothamBold']), + TextXAlignment: mkE(['Left','Center','Right']), + TextYAlignment: mkE(['Top','Center','Bottom']), + ScaleType: mkE(['Stretch','Slice','Tile','Fit','Crop']), + AspectType: mkE(['FitWithinMaxSize','ScaleWithParentSize']), + DominantAxis: mkE(['Width','Height']), + BorderMode: mkE(['Outline','Middle','Inset']), + FormFactor: mkE(['Symmetric','Brick','Plate','Custom']), + PartType: mkE(['Ball','Block','Cylinder','Wedge','CornerWedge']), + SurfaceType: mkE(['Smooth','Glue','Weld','Studs','Inlet','Universal']), + ContextActionResult: mkE(['Pass','Sink']), + UserInputState: mkE(['Begin','Change','End','Cancel','None']), + }); + + // TweenInfo — конструктор объекта с параметрами анимации + // Сигнатура: TweenInfo.new(time, easingStyle, easingDirection, repeatCount, reverses, delayTime) + global.set('TweenInfo', { + new(time, easingStyle, easingDirection, repeatCount, reverses, delayTime) { + return { + Time: time || 1, + EasingStyle: easingStyle, + EasingDirection: easingDirection, + RepeatCount: repeatCount || 0, + Reverses: !!reverses, + DelayTime: delayTime || 0, + }; + }, + }); + + // NumberSequence, ColorSequence — упрощённые конструкторы для GUI-эффектов + global.set('NumberSequence', { + new(...args) { return { Keypoints: [], __ns: true }; }, + }); + global.set('ColorSequence', { + new(...args) { return { Keypoints: [], __cs: true }; }, + }); + global.set('NumberRange', { + new(min, max) { return { Min: min, Max: max == null ? min : max }; }, + }); + global.set('Rect', { + new(minX, minY, maxX, maxY) { return { Min: { X: minX, Y: minY }, Max: { X: maxX, Y: maxY } }; }, }); // === print / warn ===