studio/RUBLOX_LUA_SUPPORT_PLAN.md
min f7441b0bd6
Some checks failed
CI / Lint (push) Failing after 1m8s
CI / Build (push) Successful in 1m58s
CI / Secret scan (push) Successful in 23s
CI / PR size check (push) Has been skipped
CI / Deploy to S1 + S2 (push) Successful in 3m28s
feat: 50 ��� �� Lua + ������ Roblox ��� ���� + ������ ����
2026-06-09 21:59:19 +00:00

25 KiB
Raw Blame History

План: Полная поддержка 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: javascriptlua
  • Подсветка/автодополнение Lua встроены в Monaco (monaco-editor/esm/vs/basic-languages/lua)
  • Линтер ошибок Lua через luaparse (npm пакет, ~50KB, parse-only) — показываем красные подчёркивания
  • Шаблон Lua для нового скрипта на target=Part:
    -- Скрипт на части. script.Parent = эта часть.
    local part = script.Parent
    part.Touched:Connect(function(hit)
        print("Касание:", hit.Name)
    end)
    
  • Шаблон Lua для глобального скрипта (target=nil):
    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:

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 вопросам перед стартом.