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

435 lines
25 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# План: Полная поддержка 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 вопросам перед стартом.