gmod-rs/docs/LUA.md
Niko Marmeladkov a3b884b7c6 Q-меню: hold/pin, кэш иконок, размер окна и сохранение скорости
Иконки: видимая сетка оставалась белыми заглушками, потому что при
бюджете 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 до/после.
2026-10-24 22:56:26 +03:00

17 KiB
Raw Permalink 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), а для клавиатуры — 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 сразу» — идём по приоритетам и дампам.