# План: Полная поддержка Lua-скриптов в Рублоксе **Цель:** Пользователь Рублокс-студии создаёт скрипт → выбирает язык **JS** или **Lua** → пишет код → в плеере оба языка работают параллельно. Lua-скрипты совместимы с Roblox API настолько, чтобы код из Roblox-игр работал без модификаций. **Зачем:** В Roblox экосистеме сотни тысяч разработчиков, привыкших к Lua + Roblox API (Vector3, CFrame, Instance, RemoteEvent, etc.). Сейчас они не могут перенести свои игры. С этой фичей — могут. **Срок:** ~6 недель полного времени. Можно делать поэтапно (после каждого этапа есть полезный результат). **Юр.риск:** "юр риски беру на себя" (МИН, 2026-06-08). --- ## Архитектурное решение ### Текущая ситуация - Скрипты хранятся как `{id, code, target, name}` в БД - `GameRuntime` запускает каждый JS-скрипт в Web Worker `ScriptSandboxWorker.js` - API игре доступен через `game.*` объект (события, scene, player, gui, save, etc.) - Скриптов в игре могут быть **сотни**, каждый — отдельный Worker ### Целевая - Скрипты получают поле `language: 'js' | 'lua'` (default `'js'`) - В редакторе — переключатель в `ScriptEditor.jsx` в шапке: **JS / Lua** - Monaco-editor подсвечивает Lua (есть встроенный язык в `monaco-editor`) - В runtime: JS-скрипты идут через старый sandbox, Lua — через новый `LuaSandbox.js` - Lua-runtime построен поверх **wasmoon** (Lua 5.4 в WebAssembly) - Lua-скрипты делятся на **один shared VM** на игру (а не Web Worker на скрипт) — иначе OOM при 100+ скриптах - Lua-API проксирует Roblox API → нашу game.* плюс полную **DataModel** иерархию ### Ключевая идея **Lua-скрипты не вызывают `game.move(...)`. Они вызывают `script.Parent.Position = Vector3.new(1,2,3)`** — как в Roblox. Lua-shim под капотом переводит это в `partSet` команды для движка Рублокса. Юзер пишет идиоматичный Roblox-Lua, движок Рублокса исполняет. --- ## Этап 1: UI и хранение языка (3 дня) ### 1.1 Миграция БД - Добавить поле `language VARCHAR(8) DEFAULT 'js' NOT NULL` в таблицу `scripts` (есть на storys API) - API endpoints (`POST/PATCH /scripts/...`) принимают и сохраняют `language` - Старые скрипты без поля → `'js'` по умолчанию ### 1.2 ScriptEditor.jsx - Переключатель в шапке редактора: `[ JS | Lua ]` (segmented control) - При смене языка — confirm("Сменить язык? Текущий код будет очищен"), затем код сбрасывается на шаблон-заглушку нового языка - Monaco language switch: `javascript` ↔ `lua` - Подсветка/автодополнение Lua встроены в Monaco (`monaco-editor/esm/vs/basic-languages/lua`) - Линтер ошибок Lua через **luaparse** (npm пакет, ~50KB, parse-only) — показываем красные подчёркивания - Шаблон Lua для нового скрипта на target=Part: ```lua -- Скрипт на части. script.Parent = эта часть. local part = script.Parent part.Touched:Connect(function(hit) print("Касание:", hit.Name) end) ``` - Шаблон Lua для глобального скрипта (target=nil): ```lua local Players = game:GetService("Players") Players.PlayerAdded:Connect(function(player) print("Игрок зашёл:", player.Name) end) ``` ### 1.3 Иконка языка в HierarchyPanel - Рядом с именем скрипта — маленький бейдж `JS` или `Lua` (синий / голубой) - Помогает не путаться при сотне скриптов ### Что готово в конце Этапа 1 - Юзер может **создать Lua-скрипт**, написать код, сохранить - Код **не исполняется** — пока только хранится и редактируется - В плеере Lua-скрипты молча игнорируются --- ## Этап 2: Базовый Lua-runtime (5 дней) ### 2.1 LuaSharedSandbox.js — новый sandbox-класс Полная архитектура шаринг-VM (один wasmoon-state на все Lua-скрипты игры): ``` GameRuntime ├── ScriptSandbox (JS-скрипт, Web Worker, как сейчас) ├── ScriptSandbox (JS-скрипт) ├── LuaSharedSandbox ← НОВЫЙ │ ├── LuaSharedWorker.js │ │ └── wasmoon VM (один на всю игру) │ │ ├── Roblox API shim │ │ ├── DataModel tree (game.Workspace, Players, ...) │ │ └── все Lua-скрипты как сопрограммы (coroutines) │ └── проксирует partSet/sceneCreate/event обратно в main thread ``` Файлы: - `src/editor/engine/LuaSharedSandbox.js` (main thread): API совместимый с ScriptSandbox (`sendSceneSnapshot`, `sendGlobalEvent`, etc.) - `src/editor/engine/LuaSharedWorker.js` (Web Worker): держит wasmoon, исполняет скрипты, шлёт командные сообщения - `src/editor/engine/RobloxLuaShim.js` (worker side): объявление всех Roblox-классов и сервисов ### 2.2 Минимальный Roblox shim в первой итерации - `Vector3.new(x,y,z)`, `+`, `-`, `*`, `:Magnitude()`, `:Dot()`, `:Cross()`, `:Lerp()`, `:Normalize()` - `Color3.new(r,g,b)`, `Color3.fromRGB(r,g,b)`, `:Lerp()` - `CFrame.new(x,y,z)`, `CFrame.lookAt()`, `CFrame.fromEulerAnglesXYZ()`, операторы `*` и `:Inverse()`, `:ToWorldSpace()` - `UDim2.new(sx,ox,sy,oy)`, `UDim.new(s,o)` - `Enum.KeyCode.W`, `Enum.UserInputType.MouseButton1`, etc. (через generated table) - `print()` → console + onLog event в студии - `wait(secs)` / `task.wait(secs)` — через coroutine + scheduler в main loop - `tick()`, `os.time()`, `os.clock()`, `math.*`, `string.*`, `table.*` (стандартные Lua) - `pcall`, `xpcall`, `error`, `assert` (стандартные) ### 2.3 GameRuntime интеграция - При старте игры — пробежать по `scripts[]`, разделить на `jsScripts` и `luaScripts` - Для JS — старый путь (по сэндбоксу на скрипт) - Для Lua — один `LuaSharedSandbox`, в него `addScript(luaSource)` каждым - LuaSharedSandbox шлёт назад те же команды что JS sandbox: `partSet`, `sceneCreate`, `chatSay`, `guiSet`, etc. ### Что готово в конце Этапа 2 - Lua-скрипты **исполняются** - Можно использовать `Vector3`, `Color3`, `print()`, `wait()`, `math/string/table` - Скрипт **ещё не видит** Workspace, Player, GUI --- ## Этап 3: DataModel — game.Workspace и иерархия (5 дней) ### 3.1 Что такое DataModel В Roblox любая игра — **дерево объектов**. Корень = `game`. У него детки = сервисы: `Workspace`, `Players`, `ReplicatedStorage`, `Lighting`, `StarterGui`, `RunService`, etc. У каждого деток — свои детки. У нас сейчас сцена плоская: `primitives`, `blocks`, `models`. Нужно **виртуальное дерево DataModel** поверх плоской сцены. ### 3.2 Виртуальное дерево Файл: `src/editor/engine/datamodel/DataModelTree.js` При старте Lua-runtime, для текущей сцены строится виртуальное дерево: ``` game (RbxGame) ├── Workspace (RbxWorkspace) │ ├── Part_0 ← обёртка над primitive id=0 │ ├── Part_1 ← обёртка над primitive id=1 │ ├── Model_5 ← обёртка над model id=5 (с Children) │ │ └── Part_inner │ ├── Camera │ └── Terrain ├── Players (RbxPlayers) │ └── LocalPlayer (RbxPlayer) │ ├── Character (RbxCharacter) │ │ ├── Humanoid (RbxHumanoid) │ │ └── HumanoidRootPart (RbxPart) │ └── PlayerGui (RbxScreenGui-контейнер) │ └── (Lua-скрипты могут спавнить GUI через Instance.new) ├── ReplicatedStorage (RbxFolder) ├── ServerStorage (RbxFolder) ├── Lighting (RbxLighting) ├── StarterGui (RbxFolder) ├── StarterPlayer (RbxFolder) │ └── StarterCharacter (RbxFolder) ├── RunService (RbxRunService с :Heartbeat, :Stepped, :RenderStepped) ├── UserInputService (события InputBegan/Ended/Changed) ├── TweenService (:Create, :GetService для tweens) ├── HttpService (заглушка либо проксируем через нашего бэка) ├── DataStoreService (проксируем через game.save) └── MarketplaceService (заглушка) ``` ### 3.3 Instance-классы `src/editor/engine/datamodel/Instance.js`: ```js class RbxInstance { Name = "Instance"; ClassName = "Instance"; Parent = null; Children = []; // Свойства которые юзер может ставить через __newindex (metatable) // отслеживаются — при изменении посылается команда в main thread // для синхронизации с Babylon-сценой GetChildren() { return [...this.Children]; } FindFirstChild(name, recursive) { ... } WaitForChild(name, timeout) { ... } // через coroutine + yield FindFirstAncestor(name) { ... } FindFirstChildOfClass(class) { ... } Destroy() { ... } Clone() { ... } IsA(class) { ... } GetFullName() { ... } GetAttribute(name) / SetAttribute(name, value) GetPropertyChangedSignal(name) → RbxSignal } class RbxPart extends RbxInstance { Position // setter: партиклс в Babylon (primitiveManager.setPosition) Size // setter Color // setter Material // setter (mapping Roblox materials → наши) Anchored // setter CanCollide // setter Touched // RbxSignal — Fire когда BabylonScene детектит overlap TouchEnded // RbxSignal CFrame // computed property — Position + rotation } class RbxModel extends RbxInstance { PrimaryPart GetPrimaryPartCFrame() / SetPrimaryPartCFrame() PivotTo(cframe) // MoveTo + Rotate GetBoundingBox() } class RbxHumanoid extends RbxInstance { Health = 100 MaxHealth = 100 WalkSpeed = 16 JumpPower = 50 Died, HealthChanged, Touched, StateChanged — signals TakeDamage(amount) MoveTo(pos) // simulates Roblox NPC pathing LoadAnimation(anim) → RbxAnimationTrack } class RbxScript extends RbxInstance { Source // источник Lua (read-only обычно) Disabled // bool RunContext // Server / Client / Legacy } class RbxRemoteEvent extends RbxInstance { OnServerEvent : RbxSignal OnClientEvent : RbxSignal FireServer(...args) FireClient(player, ...args) FireAllClients(...args) } ``` ### 3.4 Метатаблицы Lua Каждая JS-обёртка `RbxPart` экспортируется в Lua как **table с metatable**: - `__index` — чтение свойства, либо метод - `__newindex` — запись свойства, триггерит side-effects (синхронизация сцены) - `__tostring` — для `print(part)` показывает `"Part_0"` Это **критично** для совместимости с Roblox-скриптами. ### 3.5 script.Parent для каждого Lua-скрипта - Если скрипт привязан к `target=42` (primitive id 42) — `script.Parent = workspace:FindFirstChild по primId(42)` - Если глобальный — `script.Parent = nil` - В скриптовом контексте: `script` это таблица `{Name=..., Parent=..., ClassName="Script"}` ### Что готово в конце Этапа 3 - Lua-скрипт может пройтись по `game.Workspace:GetChildren()` - `script.Parent.Touched:Connect(...)` работает (KillBrick = реально) - `local player = game.Players.LocalPlayer; player.Character.Humanoid.Health = 0` убивает игрока - `Instance.new("Part", workspace)` создаёт примитив --- ## Этап 4: Полный Roblox API (10 дней) Закрываем "длинный хвост" API. Каждый день — 1-2 сервиса. ### 4.1 Services - **RunService**: `.Heartbeat`, `.Stepped`, `.RenderStepped` — RbxSignals, фаер в main loop tick - **UserInputService**: `.InputBegan`, `.InputChanged`, `.InputEnded` — события KeyCode/MouseButton/Touch - **TweenService**: `:Create(instance, TweenInfo, propertyTable)` → возвращает RbxTween; `Tween:Play()/Pause()/Cancel()`; интерполируется в main loop - **DataStoreService**: проксируем через наш `game.save` (sync version) - `:GetDataStore(name)` → объект с `:GetAsync(key)`, `:SetAsync(key, value)`, `:UpdateAsync(key, fn)` - Async-методы через coroutine.yield + наш save API - **MarketplaceService**: заглушки `PromptPurchase`, `GetProductInfo` (бизнес-логика через наш интерфейс) - **HttpService**: `:JSONEncode`, `:JSONDecode`, `:GenerateGUID` — простая встроенная реализация; `:GetAsync/PostAsync` проксируем через ограниченный список разрешённых доменов - **Players**: `LocalPlayer`, `:GetPlayers()`, `PlayerAdded`/`PlayerRemoving` сигналы - **Lighting**: read-only сейчас (через `Lighting.Ambient`, `Lighting.OutdoorAmbient` ставить значения нашему envManager) - **Workspace**: `:Raycast(origin, direction, params)` → реальный raycast через PhysicsAABB; `:GetServerTimeNow()`; `CurrentCamera` ### 4.2 GUI (важно!) - `Instance.new("ScreenGui")` → если `Parent = playerGui`, регистрируется в нашем GuiManager - `TextLabel`, `TextButton`, `ImageLabel`, `ImageButton`, `Frame` — все мапятся на наш GuiOverlay - `MouseButton1Click`, `MouseEnter`, `MouseLeave`, `Activated` — сигналы - `UDim2`, `Vector2` для позиций/размеров - При установке `gui.Position = UDim2.new(0.5, 0, 0.5, 0)` — пересылается в GuiManager и обновляется DOM ### 4.3 Sound - `Instance.new("Sound", part)` с `SoundId = "rbxassetid://12345"` или с нашим URL - `:Play()`, `:Stop()`, `:Pause()`, `Volume`, `Pitch`, `Looped` - Под капотом — наш SoundManager ### 4.4 Animation - `Instance.new("Animation")` с `AnimationId` (наши собственные ID анимаций R15) - `humanoid:LoadAnimation(anim) → AnimationTrack` - `:Play()`, `:Stop()`, `:AdjustSpeed()`, `:GetMarkerReachedSignal()` - Связь с нашим R15Animator ### 4.5 Tools / Backpack - `Tool` Instance: `Activated`, `Equipped`, `Unequipped` сигналы - `player.Backpack:GetChildren()` — Lua видит инвентарь - Реализация через наш HotbarManager + InventoryService ### 4.6 ProximityPrompt, ClickDetector - ProximityPrompt: `Triggered` сигнал, `:Show/:Hide`, ActionText, ObjectText - ClickDetector: `MouseClick`, `MouseHoverEnter/Leave` сигналы ### 4.7 Networking-эмуляция (single-player) - RemoteEvent / RemoteFunction работают **локально** (поскольку у нас singleplayer на client-only) - `FireServer/InvokeServer` запускают handlers в том же VM, но в other-side контексте - Это позволяет копировать многопользовательские скрипты Roblox без изменений (хоть мультиплеера и нет) - Когда у Рублокса появится мультиплеер — `RemoteEvent` уже будет работать «по-настоящему» без изменений в скриптах юзера ### Что готово в конце Этапа 4 - **~90% типовых Roblox-скриптов работают** без модификаций - DataStore сохраняет прогресс - TweenService плавно двигает объекты - GUI создаётся скриптом - Анимации играются --- ## Этап 5: Импорт .rbxl + конвертер юзер-кода (5 дней) ### 5.1 Изменение импортера Сейчас импортер сохраняет Lua-исходник в JS-комментарии и пытается завернуть в JS-обёртку. **Это устаревает.** Новый путь: - Импортер сохраняет Lua-source **как есть** (без обёрток) - В записи скрипта `language = 'lua'` - target = primitiveId или null - В GameRuntime Lua-скрипт идёт сразу в LuaSharedSandbox ### 5.2 Конвертация ассетов - Roblox MeshId/TextureId через наш ImageProxy → ассеты сохраняются в minio + на CDN - `rbxassetid://12345` → resolve в наш asset_id - Сохранение в БД ссылок на ассеты ### 5.3 Поведение при импорте - При импорте .rbxl карты — все Lua-скрипты сохраняются как Lua-скрипты Рублокса (не пытаемся конвертить в JS) - Юзер открывает игру → редактирует → видит в Hierarchy `Script (Lua)` рядом с `Script (JS)` — может писать на любом ### Что готово в конце Этапа 5 - Импорт Roblox-карты работает **бесшовно** - Юзер может править Lua-код в редакторе и видеть результат --- ## Этап 6: Производительность и стабильность (5 дней) ### 6.1 Profiling - Замерить FPS на картах с 100/500/1000 Lua-скриптов - Если падает — переезд на **fengari** (pure-JS Lua interp, в 5-10× медленнее wasmoon но без WASM overhead на старте) либо на **собственный Lua-bytecode runtime** для горячих скриптов ### 6.2 Memory - Каждый wasmoon VM ~10-15MB. Один на игру = ОК. Если придётся разделять на части (server/client), нужно бенчить. ### 6.3 Песочница (security) - Lua не должен дёргать `io.*`, `os.execute`, `loadstring(внешний код)`, etc. - Whitelist стандартной библиотеки. Запрещаем всё что может выйти из браузера. ### 6.4 Ошибки - Lua-ошибки в скрипте — показываются в Output-панели студии (как JS-ошибки) - Stack trace с правильными номерами строк (не из обёртки) - Если скрипт зациклился — kill через `debug.sethook` после N инструкций без yield ### 6.5 Тесты - 50 unit-тестов на Roblox API (Vector3 операции, CFrame, Instance.new, Touched, RunService, TweenService) - 10 интеграционных: импортировать тест-rbxl, запустить, проверить что нужное случилось - CI: тесты прогоняются в Gitea Actions при PR ### Что готово в конце Этапа 6 - Lua-runtime production-ready - Можно объявить публично «теперь в Рублоксе пишут на Lua с Roblox-совместимостью» --- ## Этап 7: Документация (3 дня) ### 7.1 Раздел вики - `wiki/lua-intro` — введение в Lua для Рублокса (для пользователей, которые приходят с Roblox — короткое) - `wiki/lua-vs-js` — таблица: «то же самое на JS и на Lua» для типичных задач - `wiki/roblox-api-supported` — список того что работает / не работает / отличается - `wiki/lua-examples` — 20 готовых сниппетов (KillBrick, TeleportPad, Checkpoint, Coin, NPCFollower, etc.) ### 7.2 Migration guide - «Как перенести свою Roblox-игру в Рублокс» — пошаговое - Список известных несовместимостей и их обходов ### 7.3 PR-материал - Пост в /developer на team.rublox.pro: «Lua-поддержка теперь GA» - Если есть бюджет — короткий ролик на YouTube/TikTok --- ## Этапы целиком | Этап | Длительность | Содержание | |------|--------------|------------| | 1 | 3 дня | UI и хранение языка | | 2 | 5 дней | Базовый Lua-runtime + минимальный shim | | 3 | 5 дней | DataModel (game.Workspace и иерархия) | | 4 | 10 дней | Полный Roblox API (services, GUI, Sound, Animation) | | 5 | 5 дней | Импорт .rbxl и асетов | | 6 | 5 дней | Производительность, безопасность, тесты | | 7 | 3 дня | Документация и публикация | | **Итого** | **~36 рабочих дней** | **~6 недель полного времени** | --- ## Что-то можно делать раньше, чтобы получить пользу - **MVP (Этапы 1+2):** через **8 дней** — юзер может писать Lua-скрипты с минимальным API (Vector3, Color3, print, wait). Уже видит что фича есть. - **Beta (Этапы 1-3):** через **13 дней** — KillBrick'и работают, можно делать простые игры на Lua. - **GA (все этапы):** через **6 недель** — продакшен. После каждого этапа можно делать релиз и собирать фидбек. --- ## Решения которые нужны от тебя перед стартом 1. **wasmoon vs fengari** — wasmoon быстрее но WASM-heavy, fengari проще но медленнее. Предлагаю wasmoon (уже используем для импорта). 2. **Один shared VM на игру** — согласен или разделять server/client? Предлагаю один в singleplayer-фазе, разделение — позже когда будет мультиплеер. 3. **Бэкенд изменения** — нужна миграция БД (поле `language`). У нас сейчас S2 + S1 + auto-backup, ничего страшного, но согласовать момент апдейта. 4. **Roblox API trademark/copyright** — мы делаем API-compatible runtime. Названия классов `Workspace`, `Humanoid`, etc. это API names. Юр.риск есть. Ты сказал берёшь — фиксируем. 5. **Приоритет** — этот план делать **вместо** других задач (тогда параллельные фичи стопаются) или **после** текущего бэклога? --- ## Связанные документы - `RUBLOX_PROJECT.md` — общий план Рублокса - `RUBLOX_EDITOR_ROADMAP.md` — куда движется редактор - `INFO_PROCESS.md` — лог реализации (будет апдейтиться по ходу) --- **Создано:** 2026-06-08, Claude (Opus 4.7) совместно с МИНом. **Статус:** план готов, ждём решения по 5 вопросам перед стартом.