# Lua-рантайм Source RS ## 1. Базовая линия - GMod использует **LuaJIT** (семантика Lua 5.1) → берём `mlua` с feature `luajit` + `vendored`, JIT включён. - Понимание реальности: **PAC3 и Starfall — это аддоны, исполняемые в GMod Lua.** Starfall дополнительно тащит внутрь собственный Lua-интерпретатор. Значит, «поддержать Starfall/PAC3» = реализовать движковый API (`render`, RT, кости, материалы, meshes), который эти аддоны вызывают; их собственную логику пишем не мы. - Источники требований: - дампы клиентского Lua с реальных серверов через `~/glua-steal` (gluasteal); - wiki.facepunch.com/gmod; - базовые lua-файлы из установки GMod (референс bootstrap-порядка); - наш инструмент `lua-api-report` (сканирует дампы и строит рейтинг используемых, но не реализованных API). ## 2. Порядок реализации | Очередь | Цель | Ключевые модули | |---|---|---| | 1 | HUD/UI аддоны сервера | `hook`, `net`, `timer`, `util`, `concommand`, `surface`, `draw`, `render`, `cam`, `input`, `render targets` | | 2 | Derma-меню | `vgui`, `derma`, шрифты, layout, анимации, клиппинг, скролл | | 3 | PAC3 | кости/аттачменты моделей, RT, материал-оверрайды, цвет/материалы, бюджеты | | 4 | Starfall (клиентсайд) | API-паритет с тем, что использует аддон; усиленный sandbox | Правило: API строится «рельсами» — каждый модуль регистрируется декларативно (функции, сигнатуры, проверки аргументов), чтобы добавлять покрытие быстро и единообразно. ### 2.1. UI-слой VGUI/Derma (2026-10-24) Спавн-меню Sandbox строится **настоящими** скриптами `gamemodes/sandbox` и `lua/vgui/*` через минимальный слой на Rust+Lua в `crates/lua/src/ui*`: - `load_ui(game_dir)` грузит `lua/includes` (baseclass, расширения строк/таблиц, `scriptedpanels`, `panel.lua`), `lua/includes/modules/spawnmenu.lua`, настоящие `lua/derma/init.lua`, `lua/skins/default.lua`, набор `lua/vgui/*`, затем геймод и вызывает `OnGamemodeLoaded`. - Панель — Lua-таблица с методами; `vgui.Register/Create` работают по-настоящему (класс + базовый класс), `Init` каждого класса обёрнут в `pcall`, поэтому сбой одного контрола не убивает `SpawnMenu`. - `surface.*`/`draw.*` во время снимка пишут команды `rect/outline/text/image`; `__sr_ui.snapshot()` обходит видимое дерево, вызывает `Panel:Paint(w,h)` и соблюдает лимиты 4096 команд/32 уровня. `image` — 3D-превью модели: `UiCommand.model` содержит путь `models/...mdl`. - Клиппинг: `UiCommand.clip = [x0,y0,x1,y1]` — пересечение границ панели и всех родителей; `surface.DisableClipping(true)` временно его снимает (тени рамок рисуются за границей, как в GMod). Обрезка выполняется на CPU по вершинам, поэтому длинные подписи дерева не вылезают на содержимое. `paint_panel` не заходит в поддеревья за пределами clip, а `emit` не создаёт полностью обрезанные команды: снимок большой сетки не упирается в лимит 4096 и не тратит время на невидимые иконки. - Снимок заказывается по dirty-флагу: сеттеры панелей, ввод и таймеры зовут `ui.mark_dirty(layout)`. Rust каждый кадр прокачивает `ui_tick()` (таймеры), проверяет `ui_dirty()` и пересобирает снимок только при изменении; раз в 60 кадров — страховка. `layout_dirty` отдельно от отрисовки: hover меняет только картинку, колесо/перетаскивание — геометрию. Это убрало пересборку Lua-дерева раз в 6 кадров (было 30–80 мс на кадр, стало ~0,3 мс в покое). - Контракт для рендера: `LuaRuntime::ui_toggle/ui_visible/ui_snapshot/ui_mouse/ ui_dirty/ui_tick` (+`ui_set_screen`), а для клавиатуры — `ui_text_focus/ui_text_input/ui_key/ui_clear_focus`; типы `UiKind`/`UiCommand`. Координаты — пиксели экрана. - Корень `SpawnMenu` создаётся без родителя, поэтому его размер выставляет `ui.sync_root_size`: на открытии, при `ui_set_screen` и каждой раскладке он занимает `ScrW()×ScrH()`. Без этого после прогрева до создания окна меню оставалось 1280×720 на большем экране (жалоба «меню стало меньше»). - Скин: после `skins/default.lua` вызывается `ui.apply_light_skin()` — светло-серые панели, тёмный текст, голубые выделения. Настоящая текстура `gwenskin` не загружается; `surface.DrawTexturedRectUV` подбирает цвет по UV-области (панель/кнопка/вкладка/список), поэтому меню светлое, а не тёмное, как было при подмене крупных белых плиток тёмным фоном. - Ввод: поиск верхней видимой панели под курсором (учитывается вся цепочка видимости, а не только сам флаг панели), вызовы `OnMousePressed/ OnMouseReleased/OnCursorMoved/OnMouseWheeled`; `OnCursorMoved` получает координаты относительно панели, как в GMod. `MouseCapture` удерживает перетаскивание (ползунок слайдера) вне границ панели. Колесо всплывает от кнопки/иконки к родителю до `DScrollPanel`/`DTree`, пока обработчик не вернёт `true`; клик по вкладке меняет активную панель. Hover не зовёт `OnCursorEntered` (его `InvalidateLayout` раскладывал всё дерево на каждом движении мыши) — скины читают `Hovered`. - Клавиатура: клик по `TextEntry` ставит `ui.focus` и включает SDL TextInput (`SDL_StartTextInput`; в SDL3 ввод по умолчанию выключен). `Event::TextInput` вставляет символы в поле, `ui.key` обрабатывает Backspace/Delete/стрелки/ Enter. Пока поле в фокусе, `ui.text_focus()`/`input.IsKeyTrapping()` истинны, поэтому Q печатается в поле и не закрывает меню. Фокус снимается кликом мимо и при закрытии меню. - Слайдеры: настоящие `DSlider`/`DNumSlider`/`DNumberScratch` загружаются (`dnumberscratch.lua` добавлен в `VGUI_FILES`); добавлены недостающие движковые методы `NoClipping`, `GetCaretPos/SetCaretPos`, `input.SetCursorPos`. Перетаскивание меняет `SlideX` → `SetValue` → `OnValueChanged`, число показывается в `DTextEntry`. Если файл контрола отсутствует, в fallback есть минимальные `DSlider`/`DNumSlider`/`DNumberScratch`. - Текст: `surface.GetTextSize` считает ширину по тем же метрикам, что `ui_overlay::text_scale` (8 px на байт × масштаб), а вертикальное выравнивание — по фактической высоте глифа. `ContentAlignment` разобран по «цифровой клавиатуре» GMod (4/5/6 — лево/центр/право, 7/8/9 — верх); вкладки `DTab` принудительно центрируются, дерево остаётся слева с отступом. Многострочная заглушка иконки рисуется двумя строками по центру и обрезается прямоугольником самой иконки. - Клиент (`crates/client/src/map.rs`, `ui_overlay.rs`) рисует снимок поверх кадра: прямоугольники — точными квадами, текст — bitmap-шрифтом (scale = h/12), иконки — текстурированными квадами между командами снимка, цвета переводятся из sRGB в линейные. Меню модальное: Q/Esc, мышь и колесо уходят в Lua, игрок и камера стоят; на Android есть кнопка MENU и касания переводятся в пиксельные координаты. Если `g_SpawnMenu` не создан, клиент откатывается на встроенное `qmenu.rs`. - Q работает как в GMod: нажал — открылось, отпустил без действий — закрылось. Клик, колесо или ввод в поле ставят `pinned`: после этого меню ждёт повторного Q (toggle). Пока меню открыто, позиция игрока заморожена, но `velocity`/импульс не обнуляются — после закрытия движение продолжается с той же скоростью (включая noclip). - 3D-иконки (`render::ModelIcons`): модель грузится `engine::load_model`, рисуется в offscreen-текстуру 128×128 камерой «три четверти» на светлом фоне, в UI кэш LRU на 1024 пути, живущий весь сеанс. Клиент каждый кадр собирает отсутствующие видимые иконки, сортирует по площади на экране и грузит до 12 штук или ~40 мс за кадр: видимая сетка заполняется за первые секунды, дальше всё берётся из кэша. При повторном открытии меню offscreen-рендера нет. `ModelIcons::stats()` отдаёт `cached/rendered/reused/ evicted/failed`; клиент логирует их при открытии/закрытии и в профиле. Отсутствующие/битые пути запоминаются, чтобы не грузить их повторно. Пока текстуры нет, `ui_overlay` рисует светлую заглушку: имя файла в две строки по центру, обрезанное прямоугольником иконки. - Спавн из меню: `RunConsoleCommand` складывает вызовы в очередь (`LuaRuntime::take_console_commands`), клиент исполняет `gm_spawn` (и `gm_spawnsent`/`gm_spawnswep` через реестры ENT/SWEP) обычным спавном пропа. - Ограничения реализации: нет HTML-вкладок Dupes/Saves, приблизительные шрифтовые метрики (bitmap 8×8, не TTF), текстура скина `gwenskin` заменена палитрой по UV (не пиксель-в-пиксель с оригиналом). Иконки грузятся синхронно в главном потоке, но порциями по времени; первый показ большой категории может дать несколько кадров с заглушками. ## 3. Sandbox (защита от крашедла) Цель: аддон/сервер не может уронить клиент. 1. **Память:** лимит на Lua-state (аллокатор-встрел), счётчики по аддонам. 2. **Время/инструкции:** count-hook (например, каждые N тысяч инструкций) + watchdog-поток; превышение → ошибка «больше функций недоступно этому файлу», файл помечается опасным. Примечание: включение хуков на функциях отключает JIT для них — поэтому хуки активны только для **недоверенного серверного** кода; локальный код может исполняться без хуков (конвар). 3. **Запреты:** `io`, `os` (кроме безопасного минимума), `ffi`, `require` вне whitelist, `debug` (или строго ограниченный), path traversal при любых файловых API. 4. **Лимиты ресурсов:** размеры render targets, количество материалов/мешей/партиклов/ таймеров на аддон; квоты на кадр. 5. **Изоляция ошибок:** каждый hook/колбек в `catch_unwind`; ошибка Lua не роняет кадр; после N ошибок колбек отключается (как в GMod сообщение в консоль). 6. **Safe mode:** перед загрузкой файла пишем маркер «сейчас грузится X»; если прошлый запуск завершился нечисто — X в чёрный список, клиент стартует без него и сообщает. 7. **Chaos-тесты:** бесконечный цикл, рекурсия, гигабайтный mesh, битый RT, thousands of timers, path traversal — всё в CI. ## 4. Загрузка кода с сервера - Lua-файлы приходят по сетевому каналу GMod (LZMA); храним в download-кэше по хэшу. - Загрузка — только из разрешённых корней («lua/», «addons/…»), никаких абсолютных путей. - Порядок bootstrap — как в GMod: engine/gamemode-файлы сервера, затем `lua/autorun`, затем клиентские аддоны; ошибки любого файла — не фатальны. - Повторный заход на сервер не качает то, что уже в кэше (см. `NETWORK.md`). ## 5. Мосты к движку - **Vector/Angle:** userdata с метатаблицами как в GMod (арифметика, методы). - **Entity-фасад:** ленивое создание Lua-объекта сущности; `GetPos/SetPos/GetAngles…`; в hot paths — никаких аллокаций, если аддон не трогает сущность. - **net:** `net.Start/Write/Read/Receive` с совместимой сериализацией и stringtable-сжатием имён. - **render targets:** объекты с лимитами и учётом VRAM; `cam.Start3D2D`, `render.SetRenderTarget`. - **Материалы:** `Material()`, `CreateMaterial`, `render.SetMaterial`, blend-режимы — строго с compat-семантикой рендера. - **Meshes (для Starfall/аддонов):** VertexBuffer/ObjectBuffer API (если требуется аддонами). ## 6. Производительность Lua - LuaJIT + минимум C-мостов в hot paths (batch-вызовы, кэш userdata). - Hooks: быстрый вызов по числовому ID; списки — массивы без аллокаций. - Тяжёлые аддоны: опционально выносить в отдельные состояния/потоки (архитектурная заготовка с M7), но по умолчанию — как в GMod (главный поток). ## 7. Что честно не обещаем - 1:1 Derma во всех деталях с первого раза — итерации по реальным аддонам. - Нативные бинарные модули Lua (`.dll`/`.so` аддонов) — не поддерживаем. - Полный паритет API «всё из wiki сразу» — идём по приоритетам и дампам.