gmod-rs/docs/LUA.md
Niko Marmeladkov 1b24f280d3 Q-меню: dirty-снимок, слайдеры, колесо и выравнивание текста
Снимок Derma пересобирается по dirty-флагу (ввод, таймеры, сеттеры), а не раз в 6 кадров: в покое 0.26-0.5 мс вместо 4-6 мс, без пиков 47-84 мс. Невидимые поддеревья и обрезанные команды не попадают в снимок; сортировка детей без ZPos не аллоцирует; hover не перезапускает раскладку. Меню прогревается на загрузке карты.

Слайдеры: загружается dnumberscratch.lua, добавлены NoClipping/GetCaretPos/SetCaretPos/input.SetCursorPos, OnCursorMoved получает локальные координаты, MouseCapture удерживает перетаскивание; есть fallback DSlider/DNumSlider/DNumberScratch.

Колесо всплывает от иконки/кнопки до DScrollPanel/DTree; починен корень DTree (высота ChildNodes до VBar). Клик/колесо проверяются тестами и отладочными SOURCE_RS_UI_CLICK/WHEEL.

Текст: метрики surface.GetTextSize совпадают с bitmap-рендером, вертикаль по высоте глифа, ContentAlignment как в GMod, вкладки центрируются, дерево слева с отступом, заглушки иконок - две строки по центру с обрезкой.

Иконки: очередь видимых моделей по площади, до 8/~8 мс за кадр, LRU-кэш переиспользуется.
2026-10-24 20:37:18 +03:00

15 KiB
Raw Blame History

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), типы UiKind/UiCommand. Координаты — пиксели экрана.
  • Скин: после 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.
  • Слайдеры: настоящие 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.
  • 3D-иконки (render::ModelIcons): модель грузится engine::load_model, рисуется в offscreen-текстуру 128×128 камерой «три четверти» на светлом фоне, в UI кэш LRU на 512 путей. Клиент каждый кадр собирает отсутствующие видимые иконки, сортирует по площади на экране и грузит до 8 штук или ~8 мс за кадр — очередь доходит до всей категории, а не только до первых трёх. Отсутствующие/битые пути запоминаются, чтобы не грузить их повторно. Пока текстуры нет, 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 сразу» — идём по приоритетам и дампам.