Снимок 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-кэш переиспользуется.
15 KiB
15 KiB
Lua-рантайм Source RS
1. Базовая линия
- GMod использует LuaJIT (семантика Lua 5.1) → берём
mluaс featureluajit+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).
- дампы клиентского Lua с реальных серверов через
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 (защита от крашедла)
Цель: аддон/сервер не может уронить клиент.
- Память: лимит на Lua-state (аллокатор-встрел), счётчики по аддонам.
- Время/инструкции: count-hook (например, каждые N тысяч инструкций) + watchdog-поток; превышение → ошибка «больше функций недоступно этому файлу», файл помечается опасным. Примечание: включение хуков на функциях отключает JIT для них — поэтому хуки активны только для недоверенного серверного кода; локальный код может исполняться без хуков (конвар).
- Запреты:
io,os(кроме безопасного минимума),ffi,requireвне whitelist,debug(или строго ограниченный), path traversal при любых файловых API. - Лимиты ресурсов: размеры render targets, количество материалов/мешей/партиклов/ таймеров на аддон; квоты на кадр.
- Изоляция ошибок: каждый hook/колбек в
catch_unwind; ошибка Lua не роняет кадр; после N ошибок колбек отключается (как в GMod сообщение в консоль). - Safe mode: перед загрузкой файла пишем маркер «сейчас грузится X»; если прошлый запуск завершился нечисто — X в чёрный список, клиент стартует без него и сообщает.
- 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 сразу» — идём по приоритетам и дампам.