- Текст HUD и меню теперь из атласа Liberation Sans (SIL OFL) 30 px:
ячейки 40×40, bbox-метрики в font_metrics.rs, глифы сглажены и
пропорциональны. Advance остался 8 px, поэтому вёрстка Lua не менялась;
bitmap 8×8 («не Derma») удалён, font8x8 из зависимостей убран.
- Подписи сенсорных кнопок масштабируются от размера кнопки (до 3.2×),
а не фиксированные 10 px.
- fs pack: +20 стандартных пропов (канистры, взрывная бочка, бетонный блок,
забор, скамья, колонна, дверь, стеллаж, комоды, шкаф, ванна, матрас,
столы/ящики №2, дверное колесо, кнехт). В APK 39 моделей, в спавнлисте
45 записей, иконки без заглушек.
- Android: hideSystemBars() прячет статус-бар и навигацию (API 30+ через
WindowInsetsController, старые — SYSTEM_UI_FLAG_*), повторно на
onWindowFocusChanged. ASSET_VERSION=4.
- Проверки: clippy -D warnings, cargo test --workspace — зелёные.
- APK: полные lua/ + gamemodes/ + settings + resource/language (раньше был
только autorun, и Sandbox-меню не собиралось, показывалось резервное).
ASSET_VERSION=3: при обновлении старые распакованные файлы удаляются,
иначе остаётся полный битый spawnlist.
- fs pack: список EXTRA_MODELS — 13 частых пропов и физган; спавнлист
отфильтрован по моделям, которые реально есть в APK (31 запись в 7
категориях вместо 7473 белых иконок).
- Сенсорное управление: Q, NOCLIP, LMB, RMB, JUMP, DUCK; полупрозрачные
рамки с подписями и подсказка стика. LMB/RMB превращаются в настоящие
события мыши (общий код физгана/оружия), noclip переключает полёт.
Кнопка X рисуется поверх Q-меню, остальные под меню скрыты; свайп вверх/
вниз в меню листает панели колесом.
- Lua-шим: list.Get для неизвестного списка возвращает пустую таблицу;
panel:Call/SetHTML/AddFunction/QueueJavascript — заглушки; добавлены
фразы vehicles/search/quick_filter и подсказки.
- Проверки: clippy -D warnings и cargo test --workspace зелёные.
- Вьюмодель больше не живёт в мире глубины: рендерер получил отдельный
конвейер (depth_compare Always, без записи глубины), батчи вьюмодели
исключаются из мира/теней и рисуются последними. Пол и стены её не режут;
«телефото» (distance 80/scale 2.12) убрано — ближняя посадка 34/0.7,
right 20, up -15.
- Толкание пропов теперь работает от желаемого направления ввода, а не от
фактической скорости: контроллер обнуляет её, упёршись в проп, из-за чего
толкание не срабатывало, пока игрок не вставал на проп. Проверка касания —
пересечение AABB, а не круг по максимальному габариту (игрок казался
«жирным»).
- В noclip толкание не вызывается, капсула игрока уводится под карту —
коллизий с пропами нет.
- Диаграмма/доки обновлены.
Проверки: clippy -D warnings, cargo test --workspace — зелёные.
- Убран feed-forward скорости цели: он складывался с реакцией на ту же
ошибку и на круговых движениях мышью давал перелёт. Осталась пружина
v = ошибка / timeToArrive (отставание 60 юнитов при 1200 — как в GMod),
тест circular_target_does_not_overshoot.
- Grab хранит ориентацию относительно камеры (кватернион): при повороте
взгляда проп поворачивается вместе с ним, E вращает в осях камеры, а не
мировые углы. Тест grab_keeps_angle_relative_to_camera.
- Вьюмодель физгана: телефото-посадка (distance 80, scale 2.12, right/up 59),
перспектива FOV 100 больше не растягивает упоры и руки; idle-анимация.
- Физика игрока: кинематическая капсула высотой 72 (add_character /
move_character), пропы не проходят сквозь игрока; луч физгана игнорирует
Character-тела (иначе начинался внутри своей капсулы). push_props толкает
пропы в сторону движения с sqrt(10/mass) — лёгкие отлетают, тяжёлые едут
медленно; замороженные и удерживаемые не толкаются; noclip уводит капсулу
под карту.
- Lua: DTextEntry IsMultiline/GetMultiline/Get- и SetEnterAllowed, заглушки
panel_meta (OpenURL) — исправлены OnKeyCodeTyped и падения таймеров меню.
Проверки: cargo fmt, clippy -D warnings, cargo test --workspace — зелёные;
новые тесты капсулы, луча, угла и кругового движения.
- Отставание пропа: в rapier 0.36 `normalized_max_linear_velocity` по
умолчанию 400, всё выше обрезалось на шаге (1200 → 400). Поднято до 100k;
тест-страж body_with_set_velocity_actually_moves (1200 за секунду).
- Управление как в GMod: колесо тянет/толкает вдоль луча (`physgun_wheelspeed`,
шаг 16, минимум 24), вращение — зажатый E + мышь (камера стоит). Держится
полная ориентация (pitch/yaw/roll), угловая скорость — по кватерниону
кратчайшей дуги, а не только yaw.
- Демпфирование: `physgun_maxSpeedDamping`/`maxAngularDamping` = 10000 —
предел изменения скорости (damp_towards), а не самой скорости.
`physgun_DampingFactor` в вики не указан, у поверхностей dampening 0.
- Undo как в оригинале: отменяется только спавн; Move/Freeze из истории
физгана убраны.
- Вьюмодель физгана: `weapon_physgun.txt` → `c_superphyscannon.mdl`, анимация
`idle` обязательна (bind-поза разваливается), посадка right 25/up −25/
dist 34/scale 0.9, ствол вдоль +X.
- CCD: пропы с `ccd_enabled(true)`, `max_ccd_substeps` 4,
`num_solver_iterations` 8 — быстрые пропы не пролетают сквозь карту
(тест на 5000 юнитов/с).
- HUD: wheel dist | E rotate.
Проверки: clippy -D warnings, cargo test --workspace зелёные; кадр
/tmp/opencode/physgun_final.png.
Иконки: видимая сетка оставалась белыми заглушками, потому что при
бюджете 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 до/после.
Откачен костыль с телепортом: это было лечение симптома.
Что найдено замером (тест body_with_set_velocity_actually_moves, ignored):
тело с заданной скоростью 1200 за шаг едет только 400 — и это НЕ контакт:
воспроизводится без пола, с friction 0 (combine rule Min), gravity_scale 0,
damping 0.000, dt=1/60 до и после шага. Скорость 400 читается уже после
world.step(), то есть шаг физики гасит её ровно втрое. Это и есть причина
отставания пропа при любой пружине.
Плюс сделано по делу: на время удержания у тела выключается трение
(set_body_friction + combine rule Min), на отпускании возвращается 0.6 —
это убирает потерю скорости о пол.
Дальше: разобраться, почему шаг Rapier делит скорость на 3 (подозрение —
настройки integration_parameters/units в нашей обёртке), затем включить
assert в тесте.
Диагностика показала: команда скорости упирается в 5000, target_v ~1200
(игрок), а error растёт до 1600 — тело физически не успевает (коллизия/
солвер), поэтому пружина бесполезна.
Добавлен порог 256 юнитов: при большей ошибке точка захвата переставляется
на цель (teleport тела), скорость обнуляется, last_target обновляется. Меньше
порога — прежняя линейная пружина. Так проп всегда остаётся у луча, как
жёсткая привязка в GMod.
Посадка вьюмодели вынесена в таблицу по модели: у w_pistol ствол вдоль +Y, у v_pistol/c_smg1 — вдоль +X, поэтому общие view_yaw/дистанция не работали. В движке появился масштаб экземпляра и полный доворот модели; для w_pistol/v_pistol/c_smg1 подобраны дистанция, смещение, масштаб и yaw, near-плоскость не режет ни одну модель.
weapon_pistol в HL2 — движковое оружие, его Lua-скрипта в ассетах нет. Добавлен встроенный SWEP (Slot 1/SlotPos 1, 18 патронов, одиночный огонь) с регистрацией в weapons и list.Get("Weapon"); пользовательский lua/weapons/weapon_pistol.lua важнее встроенного. ViewModelFOV и необязательные ViewModelOffset/Angles/Scale из SWEP перекрывают таблицу.
Проверено кадрами на Xvfb: пистолет и флечетган в кадре, ближе_near=0, выстрел 18→17, слот 1 после флечетгана и кулаков возвращает пистолет, c_arms по-прежнему скрывается. Добавлен SOURCE_RS_TEST_SLOT для синтетической клавиши слота (xdotool не доставляет клавиши в SDL-окно на Xvfb).
Пользователь: «могу улететь далеко от пропа, он просто линейно летит».
Причина: пружина считала скорость только от ошибки (`error / 0.05`), а цель
(точка на луче) едет вместе с игроком. Ошибка за кадр = путь игрока за кадр,
поэтому проп двигался со скоростью ~игрок·(dt/0.05) — на 60 fps втрое
медленнее игрока и безнадёжно отставал.
Добавлен feed-forward: скорость целевой точки (разница с прошлым кадром / dt)
складывается с пружиной и клампится `physgun_maxSpeed`. Теперь проп летит со
скоростью игрока, а пружина выбирает только остаточную ошибку. Поле
`Grab::last_target` хранит прошлую цель.
28 сьютов зелёные.
- пружина GMod: v = clamp(error / physgun_timeToArrive, physgun_maxSpeed),
линейная зависимость вместо прежнего разгона;
- дальность 4096 (physgun_maxrange), масса 500, maxAngular 5000;
- трассировка луча до коллайдера, local_offset и удержание за точку на меше;
- поворот колесом вокруг точки захвата с компенсацией omega × r;
- тесты закона скорости, луча и локальной точки; docs/DIARY.
Из ветки feat/menu (1b24f28):
- главный источник лагов — полный снимок Derma раз в 6 кадров (обход дерева,
DTileLayout по тысячам иконок, команды для обрезанных панелей, сортировка
детей, InvalidateLayout на hover). Добавлены ui.mark_dirty/ui_dirty/ui_tick,
отдельный layout_dirty, кэш снимка, culling невидимых поддеревьев, отказ от
сортировки без ZPos, hover больше не перезапускает раскладку;
замер: снимок avg 6,4/4,1 мс -> 0,26-0,5 мс, max 84/47 -> 18-27 мс только на
страховочной пересборке; FPS с меню и без — в пределах разброса;
- слайдеры: dnumberscratch, NoClipping, GetCaretPos/SetCaretPos,
input.SetCursorPos, локальные координаты OnCursorMoved, MouseCapture;
проверено на настоящем DNumSlider (5 -> 9,53);
- колесо: всплытие до DScrollPanel/DTree, починен корень DTree (высота
ChildNodes до VBar), тесты прокрутки;
- выравнивание: surface.GetTextSize по bitmap-метрике, ContentAlignment как в
GMod, центрирование вкладок и подписей иконок, дерево с отступом;
- иконки: очередь по площади, до 8 за кадр (~8 мс), LRU переиспользуется.
Проверено: 28 сьютов (включая 9 UI-тестов на реальных Derma-классах) зелёные;
кадры menu_fix.png, menu_tree_wheel.png, menu_placeholders.png.
Снимок 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-кэш переиспользуется.
Пользователь: «от Q-меню ужасно лагает, во время лагов нажал Escape и игра
закрылась».
- снимок панелей (Lua + строки) пересобирается раз в 6 кадров и сразу после
любого ввода в меню (движение/кнопки/колесо), а не каждый кадр — это и было
основным источником лагов;
- Esc теперь только закрывает меню; выхода из клиента по Esc больше нет
(в GMod Esc открывает/закрывает меню, а не завершает игру). Закрыть клиент
можно кнопкой окна.
В crates/client подключён UI-слой из glua: после Lua-инициализации
вызывается load_ui, и если OnGamemodeLoaded создал g_SpawnMenu, Q
открывает настоящее меню Sandbox, иначе остаётся qmenu.rs.
Рендер: ui_overlay переводит UiCommand в push_rect/push_text, цвета
sRGB→linear, бюджет глифов и отсечение за экраном. push_rect теперь
точный квад, а не растянутый глиф: тонкие линии дерева больше не
превращаются в квадраты.
Ввод: Q/Esc, мышь и колесо уходят в ui_mouse, игрок и камера стоят,
ЛКМ не стреляет; ui_set_screen на старте и resize. На Android добавлена
кнопка MENU и перевод касаний в пиксельные координаты.
Спавн: RunConsoleCommand складывает вызовы в очередь; клиент исполняет
gm_spawn (и gm_spawnsent/gm_spawnswep) обычным спавном пропа на полу
перед игроком.
Попутно: ui_snapshot_into переиспользует буфер, ui_take_errors отдаёт
ошибки панелей в лог; в VGUI-слое добавлены HasChildren/ChildrenSize/
HasFocus/GetTextSize/DrawTextEntryText, без которых сетка иконок имела
высоту 0 и клики не попадали.
- map.rs: объединены ветки ввода. Пока меню открыто, Esc/Q/цифры/ЛКМ
принадлежат меню (клик выбирает предмет, игрок стоит), иначе работают
SWEP-атака и физган; отпускание ЛКМ снимает primary_up у SWEP и не
трогает физган при открытом меню. Спавн из меню и по G идёт общим
SpawnRequest с досборкой рендерера. HUD показывает активное оружие и
подсказку с Q; вьюмодель SWEP сохранена.
- lua/lib.rs: сохранены и API SWEP (weapon_number, weapon_bool,
set_active_weapon, cur_time, weapon_field, weapon_table), и ENT-API
(entity_string, entity_bool), и лексер/препроцессор continue
(Token, preprocess_continue, is_statement_continue, read_script),
и find_model_literal для спавн-меню.
- render: push_rect и сплошной глиф для заливки оверлея.
Проверено: cargo fmt, clippy --workspace --all-targets -D warnings,
cargo test --workspace; glua с ассетами — оружия 3, энтити 6, ошибок 0;
engine world_real — 21 тест; кадры Q-меню (арбуз заспавнен перед камерой)
и SWEP (weapon_fists в HUD). При открытом меню ЛКМ не стреляет, после
закрытия ЛКМ вызывает attack: weapon_fists.
Из ветки feat/swep (a46e4d8, 1fe36b6, 5a8f971):
- выбор SWEP из list.Get('Weapon') (фильтр Spawnable + существующая
ViewModel), слоты 1..9, SOURCE_RS_SWEP для старта, Holster/Deploy при смене,
вьюмодель из SWEP.ViewModel (SOURCE_RS_VIEW_MODEL — override);
- ЛКМ → PrimaryAttack через call_weapon_method, автоматический/одиночный огонь,
Think каждый кадр; без SWEP — прежние физган и fire-анимация;
- HUD: PrintName и патроны Clip1/ClipSize;
- заглушки GMod-API под реальные скрипты: CurTime, IsValid, NULL, player.GetAll,
util.TraceLine/TraceHull/TraceEntity/TracePhysics, trace.*, sound.*/EmitSound,
константы PLAYER_ATTACK*/ACT_VM_*/MASK_*, база SWEP (GetOwner/GetViewModel/
NetworkVar/SetNextPrimaryFire/SetHoldType/Clip1/EmitSound/SendWeaponAnim/
SequenceDuration/ShootEffects/Remove), game.SinglePlayer как функция,
GetConVar для sv_defaultdeployspeed/phys_pushscale;
- тест real_gmod_weapon_methods_run_without_errors прогоняет настоящие
weapon_fists/flechettegun/medkit (SetupDataTables..Holster) без ошибок Lua.
Конфликт в crates/lua/tests/scripts.rs разрешён: сохранены тесты include и
геймода из feat/lua-pre и добавлен тест SWEP из feat/swep.
Проверено: 27 сьютов, 46+4+4 теста glua зелёные.
- weapon.rs: реестр list.Get("Weapon") с фильтром Spawnable/ViewModel,
слоты Slot/SlotPos, SOURCE_RS_SWEP, Holster/Deploy при смене;
- map.rs: клавиши 0..9 выбирают слот, вьюмодель пересоздаётся под
SWEP.ViewModel (SOURCE_RS_VIEW_MODEL остаётся override), ЛКМ вызывает
PrimaryAttack, автоматический огонь по Primary.Automatic и
GetNextPrimaryFire, Think каждый кадр;
- HUD: PrintName активного оружия и патроны Clip1/ClipSize;
- SOURCE_RS_TEST_ATTACK и SOURCE_RS_TEST_SWITCH для проверки без мыши;
- ошибка Lua в методах оружия логируется, кадр не падает.
Пользователь: в оригинале Ctrl в ноуклипе замедляет движение. Убрано
`move_up -= 1` по Ctrl; скорость: Shift ×3, Ctrl ×0.35, иначе обычная.
Плюс тесты-сканы карты: поверхности с «зеркальными» материалами (0 в зале) и
сущности рядом с залом — подтверждают, что func_reflective_glass один
(model *6, точка -2064 -2076 -286).
- библиотека `list` (Set/Add/Get/GetEntry/HasEntry) — реестры GMod, на них
держатся списки оружия, энтити и спавна;
- `weapons.Register/Get/GetStored` и `scripted_ents.Register/Get`, запись в
`list.Get("Weapon")` и `list.Get("SpawnableEntities")`;
- загрузчик `LuaRuntime::load_scripts(game_dir)`: выполняет `lua/weapons/*.lua`
(со свежей `SWEP`, заранее созданными `Primary`/`Secondary`) и
`lua/entities/*.lua` (с `ENT`), ошибка одного файла не роняет остальные;
размер файла ограничен 1 МиБ;
- заглушки, без которых скрипты не запускаются: AddCSLuaFile, Model, Sound,
include, DEFINE_BASECLASS, Material, game.AddParticles; GMod-синтаксис
проходит через препроцессор (`!` → `not`);
- клиент загружает скрипты при старте: на настоящих ассетах — 3 оружия
(fists/flechettegun/medkit), 5 энтити; `weapons.Get('weapon_fists')` читает
Primary.ClipSize и ViewModel.
Тест `real_gmod_weapons_and_entities_load` (пропускается без ассетов).
Осталось: `continue` в препроцессоре (ломает widget_bones), include базовых
классов, привязка SWEP к вьюмодели и Q-меню.
26 сьютов зелёные.
- сборка мира: модель зеркала помечается материалом-меткой `<mirror>` до
резолва материала (у reflectiveglass001 нет VTF, иначе грань выпадала),
из её граней строится плоскость MirrorPlane (origin, normal);
- рендер: проход отражённой камерой в текстуру Rgba16Float, клип по плоскости
зеркала (vec4 в uniform камеры, discard в шейдере), грань зеркала сэмплирует
отражение по экранным координатам;
- отражение привязано на слот лайтмапы (группа 3) отдельным bind group:
у устройства max_bind_groups = 4, пятая группа не создаётся;
- обычный проход пропускает зеркальные батчи (иначе чекер-заглушка
перекрывала отражение), зеркало рисуется после непрозрачного мира;
- найден и исправлен пропуск слота `sun` в Camera у world.wgsl (из-за него
fog_color читался со сдвигом).
Проверено: 25 сьютов и 29 картовых тестов зелёные; UV-градиент на грани
подтвердил плоскость и шейдер, отражение показывает стену и потолок.
Пользователь: «диван в светлой комнате тёмный, чуть ближе к свету — светлый,
хотя пол одинаковый» и «свет обновляется резко».
Причины две:
1. Модели вообще не получали свет карты: у них `lightmap_uv` указывал на белый
тексель атласа, то есть ambient + солнце без привязки к месту. Теперь при
сборке мира собираются `light_samples` (центр грани + средний свет её блока
лайтмапы), а модели берут ambient в своей точке с плавным ядром по
расстоянию (радиус 256, ядро 1/(d²+256)), с нижним порогом 0,35 — одна
ближайшая грань давала скачки «тёмный/светлый». `lightmap_uv.x < 0` —
метка шейдеру брать свет из вершины; свет обновляется и при движении
(rewrite_instance и play_model_animation).
2. Каскад теней был 2600 юнитов вокруг камеры: объект у стены выходил за его
границу и «вспыхивал» при приближении камеры. Теперь 4200 юнитов и карта
4096² — вся игровая зона gm_construct попадает в тень.
Все сьюты зелёные.
- PostProcess: 3D-текстура LUT 32³ Rgba8Unorm (по умолчанию тождественная),
сэмплер, биндинги 4/5 в композите; set_color_lut принимает raw Source
(32³ RGB, красный меняется быстрее всего) и включает флаг use_lut.
- post.wgsl: после ACES цвет переводится в sRGB, сэмплируется LUT и
возвращается в линейный (поверхность sRGB-ная).
- Клиент: читает materials/colorcorrection/<карта>.raw из VFS; для
gm_construct файл есть. Отключение — SOURCE_RS_NO_COLORCORRECTION=1.
- Замер: gm_construct.raw нейтральный (0/66/132/195/255 по диагонали), то есть
грейда у карты нет by design — цветокор включён, но визуально не меняет.
- Тест real_map_visual_entities: на карте есть func_reflective_glass (зеркало),
env_tonemap_controller, 19 infodecal, env_sky/tonemap — записано в дневник.
Все сьюты зелёные.
Пользователь видел пистолет «повёрнутым на 90° влево». Причина: клиент
выбирал для вьюмодели анимацию `idle`, а у `w_pistol` единственная `idle` —
это `@idle` мировой модели, где RAWROT2/RAWPOS задают позу **брошенного на
землю** пистолета (ствол вбок, меш на боку). В руках эта поза не годится.
Теперь для вьюмодели берутся только `idle01`/`idle01empty` (у моделей `v_*`);
если их нет — остаёмся в позе покоя, которую доворачивает `model_yaw` (-90°).
Замер в осях камеры (новый диагностический лог SOURCE_RS_VIEW_DEBUG):
до правки «вперёд 1,7 / вправо 10,6» (ствол вбок), после — «вперёд 10,6 /
вправо 1,7» (ствол вперёд). Кадр подтверждает: пистолет смотрит вперёд, виден
сзади, рукоять вниз.
tier::paths::detect признаёт корень ассетов по garrysmod_dir.vpk или
garrysmod/gameinfo.txt. В минимальном наборе нет ни VPK, ни gameinfo, поэтому
клиент с --assets <pack> отвечал «ассеты Garry's Mod не найдены», хотя все
файлы на месте. Теперь fs pack пишет gameinfo.txt из VFS, и распакованный
APK-набор загружается без полной установки.
Замер: для gm_construct реально читаются 335 файлов из VPK на 27,0 МБ
(277 материалов, 23 модели, 35 Lua-скриптов; крупнейшее — m_anm.ani 6,8 МБ)
плюс карта 35,0 МБ. Итого 62,1 МБ сырых, 31 МБ в tar.gz — против 6,7 ГБ
полной установки. Этого набора достаточно: клиент запускается с ним, карта
и пистолет полностью текстурированы (проверено кадром).
- VirtualFs: запись успешных чтений (start_recording/take_recorded);
- команда `gmod-rs fs pack <карта> --out <каталог>`: собирает мир, грузит
модели игрока и оружия, автозагрузку Lua и копирует прочитанные файлы в
структуру garrysmod/... плюс карту — получается самодостаточный корень
ассетов для `--assets`;
- android/build.sh: если есть android/assets/gmod (или GMOD_RS_ANDROID_ASSETS),
каталог кладётся в APK как assets/gmod;
- MainActivity: при первом запуске распаковывает assets/gmod в
files/gmod_server/server, если карты там ещё нет — APK самодостаточен,
adb push не нужен;
- .gitignore: android/assets/ (ассеты GMod не коммитим).
Все сьюты зелёные.
Из ветки feat/hdr (dbecb3c, 1a34203, 74db5f5, dec8b67):
- свет по полигонам: LUMP_LIGHTING раскладывается в атлас Rgba16Float
(полочная упаковка, рамка 1 тексель), у вершины появился lightmap_uv,
шейдер мира читает атлас на каждый пиксель с билинейной фильтрацией.
На gm_construct: 6891 грань, 751 718 люкселей, атлас 2048² (32 МиБ).
Фолбэк на старый вершинный свет, если атлас не собрался;
- HDR: мир/небо/вода в Rgba16Float, композит экспозиция → ACES → bloom
(порог в ½ разрешения, два прохода, аддитивно), UI отдельно в sRGB;
- тени: PCF 9 отсчётов (центр + два кольца) с поворотом по мировой позиции
и нормальным смещением, без acne;
- переключатели SOURCE_RS_NO_HDR/EXPOSURE/BLOOM/BLOOM_THRESHOLD/
DEBUG_SHADOW/NO_SHADOW/NO_VSYNC/LOG_FPS;
- замер A/B: до правок ~24,5 fps, с HDR ~23,7, без HDR ~24,4 — разница в
пределах разброса; узкое место — ~2661 draw call на проход, не HDR;
- тесты lightmap_atlas_maps_face_block и half_conversion_known_values.
Конфликт был только в docs/DIARY.md (взята секция из ветки).
Проверено: 25 сьютов, 29 картовых тестов, 15 world_real — зелёные; кадр
туннеля показывает плавный градиент вместо ступеней по полигонам.
Из ветки feat/android:
- клиент разделён на lib.rs (общий запуск) и main.rs; на Android собирается
cdylib с экспортом SDL_main, паника ловится catch_unwind на FFI-границе;
- steamworks и сетевая команда отключены для Android, Lua — 5.4 vendored
(LuaJIT не кросс-собирается), на десктопе остался LuaJIT;
- SDL3 собирается из исходников, Java-обвязка SDLActivity + MainActivity
(ландшафт, путь к ассетам через getExternalFilesDir);
- сенсорное управление: левый стик — ходьба, правая половина — обзор,
кнопки прыжка/приседа; короткий тап не теряется между кадрами;
- android/build.sh собирает APK без Gradle (cargo + aapt2 + d8 + zipalign +
apksigner), поддерживает ANDROID_ABI=x86_64;
- docs/ANDROID.md и android/README.md.
APK: target/android/gmod-rs-debug.apk (arm64-v8a, minSdk 31, targetSdk 35).
Проверено на эмуляторе API 35 x86_64: установка, загрузка gm_construct,
стик, свайп-обзор, прыжок. Десктопные 25 сьютов и clippy — зелёные.
- MainActivity фиксирует ландшафт (SDL перебивал манифест FULL_USER) и
передаёт путь к ассетам через getExternalFilesDir;
- короткий тап по JUMP не теряется между кадрами (jump_pending);
- HUD сдвинут внутрь от статус-бара и нижней панели;
- логгер на Android собирается без неиспользуемой ветки.
Свет больше не интерполируется по вершинам: LUMP_LIGHTING раскладывается в
атлас Rgba16Float (рамка 1 тексель на блок), в вершине появился lightmap_uv,
шейдер мира читает атлас на каждый пиксель с билинейной фильтрацией. Это
убирает перелом света по рёбрам брашей (A10). Для карт без атласа остаётся
вершинный путь fs_main_vertex.
Мир, небо и вода рисуются в Rgba16Float; затем экспозиция, ACES и bloom
(порог в ½ разрешения, два прохода размытия, аддитивно). UI и диагностика
идут отдельным проходом в sRGB-поверхность. Переключатели: SOURCE_RS_NO_HDR,
SOURCE_RS_EXPOSURE, SOURCE_RS_BLOOM, SOURCE_RS_BLOOM_THRESHOLD.
PCF теней: 9 отсчётов с поворотом по мировой позиции и нормальным смещением,
режим SOURCE_RS_DEBUG_SHADOW. Плюс SOURCE_RS_NO_VSYNC и SOURCE_RS_LOG_FPS для
замеров.
Тесты: lightmap_atlas_maps_face_block, half_conversion_known_values; все
прежние сьюты зелёные. Замер E11: ~22,1 fps до, ~21,0 с HDR (~3% на пост),
NO_HDR ~21,7.
Из ветки feat/anim (d42d646):
- распаковка постоянных дорожек: Quaternion48 (RAWROT), Quaternion64
(RAWROT2), Vector48 (RAWPOS) — раньше такие кости застревали в позе покоя
(в @taunt_muscle_base 24 дорожки из 53);
- engine::animation — выбор секвенции по состоянию: стоя/шаг/бег/присед/
воздух, 8 направлений относительно взгляда, сохранение фазы walk↔run;
- клиент: покачивание вьюмодели при ходьбе, инерция взгляда, одиночный
выстрел fire по ЛКМ, компенсация RAWPOS-сдвига корня (у v_pistol 68 юнитов).
Разрешение конфликтов: вьюмодель осталась в камерном базисе (мои правки —
смещения через up камеры, доворот model_yaw), анимации и покачивание взяты
из ветки; play_model_animation с шестым аргументом model_yaw во всех вызовах.
Проверено: 23 сьюта, 29 картовых тестов, 15 тестов world_real — зелёные.
Вьюмодель (жалобы пользователя):
- доворот ствола (-90°) теперь применяется в координатах модели, до поворота
камеры: прибавка к мировому yaw при взгляде вниз превращалась в крен;
- смещения (вправо/вниз) задаются в базисе камеры, а не мировым (0,0,-12) —
иначе при наклоне камеры вьюмодель уезжала к центру экрана («ось ниже»);
- play_model_animation принимает model_yaw (доворот модели), rewrite_instance
тоже — путь без скелета больше не теряет ориентацию.
Коллизия пропов из настоящего меша:
- Physics::add_prop_hull — выпуклая оболочка вершин модели для динамических
пропов (как vcollide в Source), вместо коробки с выступами;
- Physics::add_static_mesh — тримеш из треугольников модели для prop_static:
раньше у статических пропов коллизии не было вовсе, игрок проходил сквозь
мебель;
- PropMesh хранит индексы модели; оба места создания тел используют оболочку
(с откатом на коробку, если вершин < 4).
Все сьюты и 29 картовых тестов зелёные.
- formats: Quaternion48/Quaternion64/Vector48 (RAWROT, RAWROT2, RAWPOS),
приоритет постоянных значений в frame_pose;
- engine::animation: выбор idle/walk/run/crouch/jump по скорости, приседу,
земле и восьми направлениям относительно взгляда, сохранение фазы walk↔run;
- клиент: модель игрока играет выбранное имя, вьюмодель качается на ходу,
отстаёт при повороте и проигрывает fire по ЛКМ; центр анимированной позы
компенсирует RAWPOS корня;
- тесты: RAW на v_pistol/m_anm, имена и проигрывание секвенций, чистый выбор
и фаза; docs A26 обновлены.
Коробка-коллайдер ставилась от нуля модели, хотя геометрия часто смещена
(у куклы центр AABB на 13,5 юнита выше нуля, у стола — 9,5 вбок по X). Из-за
этого предметы висели на невидимых выступах: их держала коробка, сдвинутая
относительно меша (жалоба пользователя, кадр с парящими столом и куклой).
- Physics::add_prop_offset — смещение коллайдера в координатах модели,
поворачивается вместе с телом (Rapier ColliderBuilder::translation);
- оба места создания тел (пропы карты и spawn_prop) передают bounds_center;
- World::prop_collision_boxes — коробки для коллизии игрока с учётом центра и
поворота (AABB описанной коробки по модулю матрицы поворота); клиент больше
не собирает их вручную;
- тест physics::prop_collider_honours_model_center_offset — без смещения проп
висит на 30+ юнитов выше пола, со смещением низ ложится на пол;
- тест real_map_props_rest_on_ground — после 10 с физики низ коробки каждого
пропа на опоре (кукла лежит краем на столе, центровой луч этого не видит).
Все сьюты зелёные.
- client получил lib.rs (общий запуск) и cdylib с экспортом SDL_main;
- steamworks и mlua-фичи разведены по target cfg: на Android нет Steam,
Lua 5.4 вместо LuaJIT;
- виртуальный стик, обзор свайпом и кнопки прыжка/приседа в map;
- android/: манифест, SDLActivity Java, build.sh (aapt2+d8+apksigner).
Падение wgpu на втором спавне («Copy at offset ... overrunning the bounds»):
буферы GPU создавались под размер мира при загрузке и не росли. Появился
WorldRenderer::ensure_capacity (рост вдвое + перезаливка) и refresh_materials
(bind group для материалов, появившихся после спавна, — иначе новые батчи
молча не рисуются).
Вьюмодель:
- обновляется после player.update и camera.pos, а не до них: раньше она
отставала на кадр при ходьбе и «плыла» при повороте (жалоба пользователя);
- по умолчанию w_pistol.mdl вместо v_pistol.mdl: v_pistol у нас рисуется
развёрнутым полотном (2157 вершин, 2 меша — разбираться отдельно), а w_pistol
выглядит пистолетом;
- курс -90°: у модели ствол смотрит вдоль +Y (влево), а не +X — без доворота
пистолет лежал поперёк кадра;
- дистанция 26, сдвиг вправо/вниз, анимация idle01 (у w_pistol свои кадры
в MDL).
bone_world_transforms теперь обходит кости рекурсивно: порядок «родитель
раньше ребёнка» в файлах не гарантирован.
HUD: журнал и отладочные строки скрыты по умолчанию (клавиша `), строка
verts/tris убрана.
Плюс тесты на реальных ассетах: кости вьюмодели не разваливаются при
скиннинге (макс. сдвиг 4 юнита), структура моделей оружия, встроенные кадры.
Все сьюты зелёные, clippy чистый.
Спека декодера взята из настоящего bone_setup.cpp (Mapbase-зеркало SDK 2013).
Прошлые догадки были неверны, и это зафиксировано в docs/PARITY.md: каналы
дорожек — не компоненты кватерниона, а углы Эйлера (raw · rotscale + поза
покоя, затем AngleQuaternion); позиции — raw · posscale + bone.pos; значения
блоков не накапливаются дельтами (ExtractAnimValue идёт по блокам valid+1
записей, внутри блока value[k], дальше повтор последнего); флаг DELTA означает
относительные каналы без позы покоя.
Замеры на живых данных, три теста в tests/world_real.rs:
- @taunt_muscle_base (451 кадр, 53 дорожки): углы ≤ 4,75 рад, сдвиги позиций
≤ 28,5 юнитов, кватернионы единичные — при новой спеке нарушений нет;
- кости локальны родителю (цепочка 4,0 / 3,5 / 8,9), корень — таз на 38,6;
- police.mdl: 57 костей, своя анимация @ragdoll лежит в самом MDL
(anim_block == 0); m_anm.mdl: 1563 анимации в models/m_anm.ani (7,1 МБ);
- модель игрока, idle_all_01: из 3147 вершин 1101 сдвинулась между кадрами 0 и
30, высота тела 61 юнит и в покое, и в анимации — не схлопывается.
Движок: AnimData (буферы кадров под Arc), frame_pose, play_model_animation
(мировые позы через родителей, A·B⁻¹ по костям, скиннинг весами вершин);
кости меша ищутся в скелете анимации по именам — скелеты совпадают по именам,
но не по числу костей. Кадры читаются и из .ani, и из самого MDL.
Клиент: вьюмодель играет idle, модель игрока — idle_all_01; добавлен вид со
стороны --third-person (иначе свою модель не увидеть: в первом лице камера
внутри неё). Map(Box<MapArgs>) — против clippy::large_enum_variant.
Все 23 сьюта и 29 картовых тестов зелёные.
PropMesh теперь несёт скелет и привязку вершин, а World::skin_model_instance
пересчитывает вершины по позе костей: угол кости складывается с родительскими,
вершина смешивается по трём весам, результат пишется в общий буфер и помечается
диапазоном для заливки на GPU — тем же механизмом moved_ranges, что возят
движущиеся пропы.
Проба подключена к вьюмодели: одна кость пистолета качается по синусу.
Проверено кадром — геометрия visibly деформируется, то есть цепочка «скелет →
веса → вершины → GPU» работает целиком. Это пока проба, а не анимация:
настоящие кадры из .ani следующим шагом.
Все 23 сьюта зелёные; docs/PARITY.md A26 обновлён.
`SOURCE_RS_VIEW_MODEL=off` гасит вьюмодель, пустое значение и отсутствие
переменной — пистолет (models/weapons/v_pistol.mdl), другое значение — путь к
модели. В ассетах нет v_physgun.mdl (это модель GMod), поэтому откат идёт на
пистолет и монтировку. Замер: центр габарита пистолета (0,26, −15,84, 52,03) —
именно его вычитает посадка.
Первый срез вьюмодели: модель оружия на том же рендере, что пропы и модель
игрока. Не рисовалась по двум причинам, обе найдены кадром:
1. разворот на 180° (нужный модели игрока) уводил ствол ЗА камеру: у моделей
оружия Source геометрия вынесена вперёд по оси модели (у пистолета ~47
юнитов, у монтировки −52 по Y);
2. привязка шла к origin модели, а надо к центру её габарита: у пистолета центр
≈(0, 0, 47), поэтому ствол уезжал выше кадра. Теперь смещение считается как
`глаз + 20 вперёд + 6 вправо − 10 вниз − повёрнутый центр габарита`.
Проверено кадром: `SOURCE_RS_VIEW_MODEL=models/weapons/v_pistol.mdl` — пистолет
стоит в кадре. По умолчанию берётся пистолет: `v_physgun.mdl` в ассетах нет
(это модель GMod, а не HL2), есть откат на монтировку.
Все 23 сьюта и 29 картовых тестов зелёные; разбор и посадка записаны в
docs/DIARY.md.
Диагностика превращена в обычный картовый тест: собираем коробку-проп вручную
(VFS в тестах пуст), поднимаем телом физики на 200 юнитов, шагаем секунду и
проверяем обе половины — вершины переписались и мир перечислил их диапазон для
заливки. Это охраняет причину жалобы «физика видит, а картинка стоит»: раньше
`move_prop` писал вершины своей копией кода мимо общего помощника и диапазоны
не запоминались.
29 картовых тестов и весь workspace зелёные.