Иконки: видимая сетка оставалась белыми заглушками, потому что при бюджете 8 мс на кадр успевало загрузиться всего 1-2 превью. Бюджет поднят до 12 штук / 40 мс, кэш увеличен с 512 до 1024 текстур и живёт весь сеанс (LRU, ключ — нормализованный путь модели). Добавлены IconStats (cached/rendered/reused/evicted/failed) и логи при открытии, закрытии и выходе: повторное открытие показывает rendered_since_open=0. Размер меню: прогрев до создания окна задавал корню 1280×720, а ui_set_screen не обновлял размер (Rust звал несуществующий __sr_resize_to_screen). ui.set_screen/ui.sync_root_size теперь растягивают g_SpawnMenu на ScrW×ScrH при открытии, resize и раскладке. Скорость: при открытии меню больше не обнуляются velocity/sprint. Игрок заморожен (update не вызывается), но импульс сохраняется: до открытия и после закрытия speed одинаковый, включая noclip. Hold/pin Q: Q down открывает, Q up закрывает только без взаимодействий. Клик, колесо, касание и ввод закрепляют меню (pinned), после чего Q работает как toggle. Esc закрывает всегда. Для текстовых полей добавлены ui.focus/RequestFocus/GetKeyboardFocus, SDL3 TextInput, Backspace/стрелки/ Enter и input.IsKeyTrapping: в фокусе Q печатается, а не закрывает меню. Попутно исправлена передача аргументов hook.Run: колбэки hook.Add больше не получают имя хука первым аргументом, как в GMod. Из-за этого OnTextEntryGetFocus падал на pnl:HasParent, и StartKeyFocus не вызывался. Проверки: cargo fmt, clippy -D warnings, cargo test --workspace (28 сьютов) зелёные. Кадры 1280×720 и 1600×900: images=88/104, placeholders=0; xdotool: hold open/close, клик+keyup, ввод q, повторный Q, скорость 250.45 до/после.
17 KiB
17 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), а для клавиатуры —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 (защита от крашедла)
Цель: аддон/сервер не может уронить клиент.
- Память: лимит на 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 сразу» — идём по приоритетам и дампам.