Some checks failed
CI / checks (push) Has been cancelled
- Текст 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 — зелёные.
950 lines
81 KiB
Markdown
950 lines
81 KiB
Markdown
# Дневник агента: где я и что дальше
|
||
|
||
Живой документ. Обновляю по ходу работы, чтобы после автоматического
|
||
уплотнения контекста ничего не терялось. Коротко: состояние, замеры, следующий
|
||
шаг, грабли.
|
||
|
||
Обновлён: 2026-10-25, утро.
|
||
|
||
---
|
||
|
||
## 0.16. Мобильное управление и настоящий Q-меню в APK (2026-10-26)
|
||
|
||
- **Почему на телефоне было «непонятное меню».** В APK лежали только 35
|
||
Lua-файлов (`lua/autorun`), без `gamemodes/` — Sandbox-меню собрать было
|
||
не из чего, и клиент показывал резервное меню. В `android/assets` добавлены
|
||
полные `lua/` (277 файлов), `gamemodes/` (320), `settings/` и
|
||
`resource/language`; `ASSET_VERSION` в `MainActivity` поднят до 3, чтобы
|
||
при обновлении APK старые распакованные ассеты перезаписались.
|
||
- **Спавнлист под мобильный набор.** В APK теперь 21 модель: пропы карты,
|
||
оружие, физган и 13 частых пропов (`oildrum001`, `woodbarrel001`,
|
||
`bluebarrel001`, `wood_crate001a/002a`, `furniturechair001a`,
|
||
`furnituretable001a`, `furnituredrawer001a`, `consolebox01a`,
|
||
`furniturebed001a`, `cactus`, `metal_plate1`). `fs pack` расширен списком
|
||
`EXTRA_MODELS` (записывает модели и их материалы), а
|
||
`settings/spawnlist_default` отфильтрован по существующим моделям: 31
|
||
запись в 7 категориях вместо 7473 битых иконок.
|
||
- **Сенсорные кнопки.** Полноценный слой: Q (открыть/закрыть настоящий
|
||
Sandbox), NOCLIP, LMB, RMB, JUMP, DUCK; полупрозрачные рамки с подписями,
|
||
стик с подсказкой базы. LMB/RMB превращаются в настоящие
|
||
`MouseButtonDown/Up` (тот же код физгана/оружия), noclip — в переключение
|
||
`player.set_noclip`. Кнопка закрытия Q (`X`) рисуется поверх меню, остальные
|
||
под меню скрыты. В меню свайп вверх/вниз листает панели (колесо).
|
||
- **Lua-шим.** `list.Get` для неизвестного списка возвращает пустую таблицу
|
||
(падал `Init SpawnmenuNPCSidebarToolbox: table index is nil`); добавлены
|
||
`panel:Call/SetHTML/AddFunction/QueueJavascript` (таймеры Dupes/Saves больше
|
||
не сыпят ошибками).
|
||
- **Шрифт и вид меню.** Прежний bitmap 8×8 выглядел «не Derma». Текст теперь
|
||
рисуется из атласа TTF: `assets/font_atlas.r8` (640×320, 16×8 ячеек по
|
||
40 px, Liberation Sans 30 px, SIL OFL), метрики bbox в
|
||
`render/src/font_metrics.rs`. Квады глифов масштабируются по 8-пиксельной
|
||
строке, поэтому вся вёрстка Lua (advance 8 px) не менялась, а буквы стали
|
||
сглаженными и пропорциональными. Прямоугольники по-прежнему используют
|
||
служебный сплошной глиф.
|
||
- **Кнопки.** Подписи масштабируются от высоты кнопки (0.42·h/8, ограничение
|
||
по ширине), а не фиксированные 10 px — на телефоне читаются.
|
||
- **Android.** `MainActivity.hideSystemBars()`: на API 30+ прячет статус-бар и
|
||
навигацию через `WindowInsetsController`, на старых — `SYSTEM_UI_FLAG_*`;
|
||
повторно вызывается на `onWindowFocusChanged`. Кадры
|
||
`/tmp/opencode/font_qmenu.png`, `/tmp/opencode/font_touch.png`.
|
||
- **Ещё пропы.** В `EXTRA_MODELS` добавлены канистры, взрывная бочка,
|
||
бетонный блок, забор, скамья, колонна, дверь, стеллаж, комод, шкаф, ванна,
|
||
матрас, стол/ящик №2, колесо двери и кнехт — ассет-пак и спавнлист
|
||
пересобираются тем же `fs pack`.
|
||
- **Проверки.** Кадры `/tmp/opencode/touch_ui.png` (кнопки) и
|
||
`/tmp/opencode/mobile_qmenu2.png` (меню из APK-ассетов); юнит-тесты
|
||
раскладки кнопок и noclip.
|
||
|
||
## 0.15. Физган: чистая пружина, угол от камеры, модель; игрок толкает пропы (2026-10-25)
|
||
|
||
- **Пружина без feed-forward.** Скорость цели больше не прибавляется к
|
||
пружине: она складывалась с реакцией на ту же ошибку (двойной учёт) и на
|
||
круговых движениях мышью давала перелёт. Осталось `v = ошибка / timeToArrive`
|
||
с ограничением `maxSpeed`; при 1200 юнитов/с проп отстаёт на
|
||
`v*0.05 = 60` юнитов — как в GMod. Перелёта нет по построению: за шаг
|
||
проходится `frac ≤ 1` ошибки. Тест `circular_target_does_not_overshoot`.
|
||
- **Угол захвата.** `Grab` хранит не только точку (`local_offset`), но и
|
||
ориентацию пропа **относительно камеры** (`local_quat`, кватернион
|
||
`камера⁻¹ * проп`). Каждый кадр цель = `камера * local_quat`: при повороте
|
||
взгляда проп поворачивается вместе с ним, сохраняя угол, под которым его
|
||
взяли. `E` домножает кватернион слева (доворот в осях камеры).
|
||
Тест `grab_keeps_angle_relative_to_camera`: камера 90°→135° даёт проп 10°→55°,
|
||
E +20° при камере 90° даёт 30°.
|
||
- **Модель физгана.** Вьюмодель — `c_superphyscannon.mdl` с анимацией `idle`
|
||
(bind-поза разваливается). «Телефото» (distance 80) оказалось хуже: разброс
|
||
глубины модели при scale 2.12 делал ближние части огромными, а сама модель
|
||
всё равно стояла в мире и уходила в пол. Теперь у рендерера есть отдельный
|
||
конвейер вьюмодели (`depth_compare: Always`, без записи глубины): батчи
|
||
вьюмодели исключаются из мира/теней и рисуются последними поверх кадра.
|
||
Посадка снова близкая: distance 34, scale 0.7, right 20, up −15 — ружьё
|
||
не режется полом и стенами. Кадр `/tmp/opencode/pg_depthless.png`.
|
||
- **Игрок ↔ пропы.** В физике появилась кинематическая капсула игрока
|
||
(`add_character`, высота 72: `capsule_z(20, 16)`, центр = ступни + 36),
|
||
она едет за игроком через `set_next_kinematic_translation`. Пропы больше не
|
||
проходят сквозь игрока, в том числе брошенные. Луч физгана исключает
|
||
Character-тела (иначе начинался внутри своей капсулы и не видел цели).
|
||
Игрок толкает пропы: `sandbox::push_props` получает **желаемое**
|
||
направление из ввода (фактическая скорость обнуляется контроллером,
|
||
упёршимся в проп, — иначе толкание не работало), проверяет пересечение
|
||
AABB (а не круг по максимальному габариту — от него игрок казался «жирным»)
|
||
и задаёт скорость с `sqrt(10/mass)`: лёгкий ящик отлетает, тяжёлый еле едет.
|
||
Замороженные и удерживаемые физганом не толкаются; в noclip капсула
|
||
уводится под карту, толкание не вызывается вовсе.
|
||
- **Lua.** `DTextEntry:IsMultiline/GetMultiline/GetEnterAllowed/SetEnterAllowed`
|
||
и заглушки `panel_meta` — исправлен
|
||
`OnKeyCodeTyped: attempt to call method 'IsMultiline'`; `OpenURL` больше не
|
||
роняет таймеры меню.
|
||
- **Проверки.** `cargo clippy -D warnings`, `cargo test --workspace` зелёные;
|
||
новые тесты: капсула не пропускает проп
|
||
(`prop_does_not_pass_through_player_capsule`), луч не цепляет свою капсулу
|
||
(`raycast_ignores_player_capsule`), угловая ориентация, круговое движение.
|
||
|
||
---
|
||
|
||
## 0.13. Физган: почему отставал, управление как в GMod, модель, CCD (2026-10-24)
|
||
|
||
- **Корень отставания пропа** найден замером, а не пружиной: в rapier 0.36
|
||
`integration_parameters.normalized_max_linear_velocity` по умолчанию 400,
|
||
и любая заданная скорость выше обрезалась на шаге (1200 → 400, чистое тело
|
||
без пола/трения/гравитации). Поднято до 100 000 в `PhysicsWorld::new`;
|
||
тест-страж `body_with_set_velocity_actually_moves` (было 400, стало 1200).
|
||
Телепорт-костыль откачен.
|
||
- **Демпфирование GMod** (вики `ConVars_In_Garrysmod`): `physgun_DampingFactor`
|
||
не указан (0), но `physgun_maxSpeedDamping` и `physgun_maxAngularDamping`
|
||
= 10000 — ограничение **изменения** скорости, а не самой скорости. Реализовано
|
||
`damp_towards`: 166 юнитов/с за кадр 1/60, до 5000 тело разгоняется за 0.5 с.
|
||
Демпфирование поверхности у пропов по `scripts/surfaceproperties.txt` — 0.0.
|
||
- **Управление**: колесо теперь тянет/толкает вдоль луча (dist, шаг 16,
|
||
минимум 24), а не вращает; вращение — зажатый `E`, мышь крутит проп, камера
|
||
стоит. Держится полная ориентация (pitch/yaw/roll), угловая скорость — по
|
||
кватерниону кратчайшей дуги (`angular_velocity_towards`), а не только yaw.
|
||
- **Undo как в оригинале**: Ctrl+Z отменяет только спавн. Перемещение и
|
||
заморозка в историю не пишутся; `SandboxAction::Move`/`Freeze` удалены.
|
||
- **Модель физгана**: `scripts/weapons/weapon_physgun.txt` →
|
||
`models/weapons/c_superphyscannon.mdl` (не `v_physgun`, которого в ассетах
|
||
нет). Анимация `idle` обязательна: bind-поза модели разваливается, поэтому
|
||
`refresh_animations` ищет `idle` запасным вариантом. Посадка:
|
||
right 25, up −25, dist 34, scale 0.9, без доворота (ствол +X). Кадр
|
||
`/tmp/opencode/pg_anim_0.png` — пушка в правом нижнем углу, ствол вперёд.
|
||
- **Коллизия**: пропы получили CCD (`ccd_enabled(true)`), `max_ccd_substeps`
|
||
4, `num_solver_iterations` 8. Тест `fast_prop_does_not_tunnel_through_floor`
|
||
(5000 юнитов/с в пол — не пролетает).
|
||
- **Проверка**: 40 клиентских + 122 движковых теста, `real_model_meshes_are_coherent`
|
||
расширен `v_superphyscannon`/`c_superphyscannon` (0 вырожденных, рёбра
|
||
в габарите).
|
||
|
||
---
|
||
|
||
## 0.12. Физган: дефолты GMod и захват за точку меша (2026-10-24, ветка `feat/physgun`)
|
||
|
||
Жалобы: проп «разгонялся квадратично» и брался за origin, а не за точку под
|
||
прицелом. Разобрано по спецификации GMod/Source, реализация своя.
|
||
|
||
- **Дефолты GMod** (`wiki.facepunch.com/gmod/ConVars_In_Garrysmod`,
|
||
`physgun_*`): `physgun_maxrange 4096`, `physgun_timeToArrive 0.05`,
|
||
`physgun_maxSpeed 5000`, `physgun_maxAngular 5000`,
|
||
`physgun_maxSpeedDamping 10000`, `physgun_maxAngularDamping 10000`,
|
||
`physgun_wheelspeed 10`, `physgun_rotation_sensitivity 0.05`,
|
||
`physgun_teleportDistance 0`, `physgun_DampingFactor 0`.
|
||
Отдельного конвара массы у физгана GMod нет; 500 — верхняя граница из
|
||
физики Source (как у гравипушки) и прежнего поведения клиента.
|
||
- **Пружина.** Было экспоненциальное приближение с `k=12` и пределом 900,
|
||
стало `v = clamp(error / timeToArrive, maxSpeed)` при `timeToArrive=0.05`:
|
||
скорость строго линейна по рассогласованию (20→400, 50→1000, 100→2000
|
||
юнитов/с) и упирается в 5000. Длинный кадр не проскакивает цель:
|
||
приближение за шаг не больше 100% ошибки. Гравитация на удержании
|
||
выключена, тело сталкивается с миром. Отдельного демпфирования нет:
|
||
скорость задаётся напрямую (в GMod `physgun_DampingFactor` по умолчанию 0).
|
||
- **Захват за точку меша.** `PhysicsWorld::raycast` трассирует луч по
|
||
коллайдерам и возвращает тело + точку попадания; статика карты даёт
|
||
окклюзию. `Grab` хранит `local_offset = inverse_rotate(angles) * (hit -
|
||
origin)`. Цель удержания — точка на луче на расстоянии захвата, а желаемый
|
||
центр тела считается как `target - rotate(angles, local_offset)`. Туда же
|
||
смещены крест и конец луча. При довороте колесом линейная скорость
|
||
компенсирует `ω × r`, поэтому проп вращается вокруг захваченного угла.
|
||
- **Колесо/freeze.** Шаг колеса 15° (`physgun_wheelspeed`), угловая скорость
|
||
ограничена 5000°/с, ПКМ по-прежнему фиксирует позу и замораживает тело.
|
||
- **Проверки.** Юнит-тесты: закон скорости и кламп
|
||
(`spring_speed_is_linear_then_clamped`), длинный кадр
|
||
(`spring_step_never_overshoots_on_long_frame`), локальная точка при
|
||
повороте (`grab_point_stays_on_target_when_body_rotates`), raycast по
|
||
коллайдеру и окклюзия статикой в `engine::physics`. Кадры Xvfb:
|
||
до — луч в центр дивана (`physgun_couch_before.png`), после — луч и крест
|
||
на правом краю, диван довёрнут вокруг этой точки
|
||
(`physgun_couch.png`); `SOURCE_RS_TEST_PHYSGUN=1` на 40-м кадре
|
||
по-прежнему замораживает проп.
|
||
|
||
## 0.12. Вьюмодель: посадка по модели и встроенный пистолет (2026-10-24, ветка `feat/viewmodel`)
|
||
|
||
**Пистолет в реестре.** `weapon_pistol` в HL2 — движковое оружие, поэтому в
|
||
`lua/weapons` ассетов его нет, и слот 1 был занят только флечетганом. В
|
||
`glua` добавлен встроенный SWEP (`builtin_weapon_pistol.lua`):
|
||
`Slot=1`, `SlotPos=1`, `Spawnable`, `PrintName="Pistol"`, магазин 18,
|
||
одиночный огонь, `PrimaryAttack` уменьшает магазин и зовёт заглушки
|
||
`EmitSound`/`SendWeaponAnim`. Регистрация идёт через общий с файловыми
|
||
скриптами путь `load_weapon_source`, поэтому оружие видно и в
|
||
`list.Get("Weapon")`, и в `weapons.Get`. Если пользователь положил свой
|
||
`lua/weapons/weapon_pistol.lua`, встроенный не загружается (правило
|
||
«оригинал важнее»). `ViewModel` — `w_pistol.mdl`: у `v_pistol` в текущем
|
||
загрузчике второй меш (руки) висит отдельно от оружия; посадка для него всё
|
||
равно описана в таблице.
|
||
|
||
**Посадка.** Общие константы (дистанция 26, `view_yaw=-90`) подходили только
|
||
`w_pistol`: у `v_pistol`/`c_smg1` ствол вдоль **+X**, а не +Y. В
|
||
`client/src/viewmodel.rs` появилась таблица `model → {right, up, distance,
|
||
scale, angles}`; значения подобраны кадрами:
|
||
|
||
| модель | right | up | distance | scale | yaw |
|
||
|---|---:|---:|---:|---:|---:|
|
||
| `w_pistol` | 14 | −9 | 20 | 1.0 | −90° |
|
||
| `v_pistol` | 8 | −10 | 34 | 0.8 | 0° |
|
||
| `c_smg1` | 14 | −10 | 26 | 1.0 | 0° |
|
||
|
||
Масштаб поддержан в движке: `ModelInstance.scale`, `set_model_instance_scale`,
|
||
`play_model_animation_oriented`/`move_model_instance_oriented` (полный
|
||
pitch/yaw/roll в координатах модели). Отладочный лог `SOURCE_RS_VIEW_DEBUG`
|
||
печатает габарит уже скинненной позы в осях камеры и число вершин ближе
|
||
near-плоскости; для всех трёх моделей `ближе_near=0`.
|
||
|
||
**Поля SWEP.** `ViewModelFOV` пересчитывается в множитель масштаба (опорный
|
||
54°), `ViewModelOffset` (Source-вектор: X вперёд, Y влево, Z вверх),
|
||
`ViewModelAngles` и `ViewModelScale` перекрывают таблицу. Для этого
|
||
`weapon_field` научился читать поля `Vector`/`Angle` (userdata), а не только
|
||
таблицы.
|
||
|
||
**Кулаки.** `c_arms.mdl` по-прежнему без геометрии: `spawn_model_instance`
|
||
возвращает ошибку, вьюмодель скрывается. Переход «кулаки → пистолет»
|
||
пересоздаёт экземпляр (проверено кадром).
|
||
|
||
**Проверка (Xvfb :99, `--hidden`, 1280×720).** Кадры:
|
||
`/tmp/opencode/vm2/final_pistol.png`, `final_flech.png`,
|
||
`final_pistol_attack.png` (17/18 после выстрела), `switch_flech_to_pistol.png`,
|
||
`switch_fists_to_pistol.png`, `switch_pistol_to_fists.png`,
|
||
`vpistol_d34.png`. На Xvfb `xdotool` не доставляет клавиши в SDL-окно,
|
||
поэтому для проверки слота добавлен `SOURCE_RS_TEST_SLOT` — синтетическое
|
||
`KeyDown` того же кода, что и настоящая клавиша (на кадре
|
||
`SOURCE_RS_TEST_SLOT_FRAME`, по умолчанию 30). Юнит-тест
|
||
`slot_keys_match_gmod` проверяет обратное соответствие клавиш 0–9.
|
||
|
||
**Осталось:** отдельный проход вьюмодели с настоящим `ViewModelFOV` (сейчас
|
||
компенсация масштабом — приближение); разобраться с оторванными руками
|
||
`v_pistol` (после этого можно вернуть его во встроенный пистолет); тёмная
|
||
текстура `c_smg1` (ориентация и посадка уже верные).
|
||
## 0.14. Q-меню: hold/pin, кэш иконок и размер окна (2026-10-24, ветка `feat/qmenu2`)
|
||
|
||
- **Белые квадраты.** Снимок и `image`-команды были целы; видимая сетка
|
||
просто не успевала заполниться: бюджет загрузки был 8 мс на кадр, а одна
|
||
иконка (MDL+текстуры+offscreen) занимает десятки мс. За 90 кадров грузилось
|
||
~60 иконок, остальные оставались заглушками. Бюджет поднят до 40 мс/12 штук
|
||
за кадр; на 1280×720 и 1600×900 к 90-му кадру `placeholders=0`
|
||
(`images` = 88/104), при этом failed=0.
|
||
- **Кэш.** `MAX_ICONS` 512 → 1024, ключ — нормализованный путь модели, живёт
|
||
весь сеанс. Добавлены счётчики `IconStats` (cached/rendered/reused/evicted/
|
||
failed) и логи при открытии/закрытии/выходе. Проверка xdotool: первое
|
||
открытие `rendered=88`, повторное открытие после закрытия —
|
||
`rendered_since_open=0`, `reused` растёт.
|
||
- **Размер меню.** Причина: прогрев `ui_toggle(true)` до создания окна
|
||
задавал корню 1280×720, а `ui_set_screen(1600,900)` размер не обновлял
|
||
(Rust звал несуществующий `__sr_resize_to_screen`). Теперь `ui.set_screen`
|
||
и `ui.sync_root_size` растягивают настоящий `g_SpawnMenu` на `ScrW×ScrH`
|
||
при открытии, resize и раскладке. Кадр 1600×900 — меню на весь экран с
|
||
полями, как SpawnMenu с `Dock(FILL)`.
|
||
- **Скорость.** Убраны `player.velocity = ZERO` и `sprint = false` при
|
||
открытии/в меню. Игрок заморожен (update не вызывается), но импульс цел:
|
||
лог `speed=250.45` до открытия и после закрытия, позиция в меню не меняется
|
||
(`pos 370 132` → `370 132` → движение после Q), noclip тоже без обнуления.
|
||
Bob/анимация в меню не проигрываются.
|
||
- **Hold/pin.** Q down открывает (`q_hold`), Q up закрывает только без
|
||
взаимодействий. Клик/колесо/касание/ввод ставят `pinned`; тогда Q down
|
||
закрывает (toggle), а Q up ничего не делает. Esc закрывает всегда.
|
||
В фокусе `DTextEntry` Q печатается: клиент проверяет `ui.text_focus()`,
|
||
SDL3 TextInput включается явно; `OnTextEntryGetFocus` теперь получает
|
||
панель (исправлена передача имени хука), поэтому `StartKeyFocus` работает.
|
||
- **Проверки:** `cargo fmt`, `cargo clippy --workspace --all-targets -D
|
||
warnings`, `cargo test --workspace` (28 сьютов) — зелёные. Кадры:
|
||
`/tmp/qmenu2-final-1280f.png`, `/tmp/qmenu2-final-1600e.png`; xdotool:
|
||
hold open/close, клик+keyup оставляет меню, ввод `q` не закрывает,
|
||
повторный Q закрывает, скорость сохраняется.
|
||
|
||
---
|
||
|
||
## 0.11. Модели: локальные индексы вершин и пол под пропами (2026-10-24, ветка `feat/models`)
|
||
|
||
- Причина «взрывов» мешей: `origMeshVertID` в VTX — локальный индекс внутри
|
||
**меша**, а загрузчик читал его как глобальный индекс VVD. У v_pistol два
|
||
меша (оружие + руки), второй брал вершины первого: рёбра растягивались с
|
||
4.4 до 27.4 юнита, появлялись вырожденные треугольники. У c_smg1 и
|
||
многоматериальных пропов — то же. Второй слой: `mstudiomodel_t.vertex_index`
|
||
(байты, /48) задаёт начало вершин модели в VVD для разных бодипартов.
|
||
- Разбор MDL расширен: `mstudiomodel_t` (172 байта) и `mstudiomesh_t`
|
||
(116 байт), `Mdl::body_part_models` отдаёт `material`, `vertex_offset`,
|
||
`num_vertices`; материал меша теперь берётся из MDL, а не из списка замен
|
||
VTX (тот относится к скинам; у пропов gm_construct пуст).
|
||
- Замеры v_pistol mesh1: было max edge 27.39 / 22 вырожденных из 816,
|
||
стало 4.43 / 0. c_smg1 mesh1: 21.51 / 19 → 14.00 / 0. По props_c17:
|
||
23 многомешевых модели, у 8 старый код растягивал детали (fence01a:
|
||
156 → 4.77 юнита).
|
||
- Физика: при создании динамического пропа низ AABB/оболочки поднимается до
|
||
статической опоры (проникновение + 0.05), чтобы Rapier не выталкивал тело
|
||
из пола первые кадры. Тест `real_map_props_rest_on_ground` теперь меряет
|
||
проникновение лучом из центра с исключением собственного тела.
|
||
- Страж: `real_model_meshes_are_coherent` проверяет вырожденные треугольники,
|
||
максимальное ребро относительно габарита меша VVD и попадание вершин в
|
||
диапазон своего меша (v_pistol, c_smg1, w_pistol, игрок, 4 пропа).
|
||
|
||
## 0.11. Sandbox: физган, freeze и undo (2026-10-24, ветка `feat/sandbox`)
|
||
|
||
Физган стал отдельным инструментом Sandbox, а не «побочным эффектом ЛКМ».
|
||
|
||
- **Выбор:** `F` включает/выключает физган. Если SWEP не выбран, физган
|
||
активен сразу (прежнее поведение). Слоты `0-9` по-прежнему выбирают SWEP и
|
||
выключают физган. `SOURCE_RS_PHYSGUN=1` включает принудительно и держит
|
||
ближайший проп без мыши.
|
||
- **ЛКМ:** захват ближайшего динамического пропа по лучу (дальность 250,
|
||
допуск 40 по перпендикуляру, масса до 500, игрок и статика не берутся).
|
||
Удержание — скоростью: экспоненциальное приближение к точке на луче,
|
||
гравитация на время захвата выключена. Это не телепорт: проп сталкивается с
|
||
миром и плавно догоняет цель.
|
||
- **Колесо:** доворачивает `target_yaw`; тело догоняет угол угловой скоростью
|
||
(до 720°/с), а не мгновенной позой.
|
||
- **ПКМ:** заморозка. Удерживаемый проп фиксируется и отпускается; свободный
|
||
под прицелом переключается `fixed` ↔ `dynamic` (аналог `PhysgunFreeze`).
|
||
Замороженные пропы помечены жёлтым крестом, чтобы freeze был виден кадром.
|
||
- **Визуал:** пучок из пяти линий от смещённого «дула» (справа-снизу, как
|
||
вьюмодель) к пропу + крест в точке захвата. Одиночная линия из центра глаз
|
||
при взгляде прямо на проп проецируется в точку и не видна.
|
||
- **Undo:** стек на 32 действия, `Z` — откат, `Shift+Z` — повтор.
|
||
Откатывает: спавн (удаляет тело/сущность и уводит меш под карту),
|
||
перемещение/поворот физганом (возвращает позу), заморозку (возвращает
|
||
состояние). Новое действие обнуляет redo.
|
||
- **Грабли удаления:** вырезать вершины из общих буферов дорого и ломает
|
||
индексы батчей, поэтому `World::remove_prop` прячет геометрию на
|
||
`z=-100000`, а `restore_prop` возвращает её и пересоздаёт сущность/тело.
|
||
Визуально проп исчезает, но память под его вершины остаётся — осознанный
|
||
компромисс до общей системы удаления сущностей.
|
||
- **Бинды/HUD:** нижняя подсказка теперь в две строки:
|
||
`WASD/Space/Ctrl/V/F/ЛКМ/ПКМ/колесо` и
|
||
`G spawn | Q menu | Z undo | Shift+Z redo | 0-9 weapon | \` log`.
|
||
В строке режима — `physgun [F] HOLD <дистанция> undo <n>/<m>`.
|
||
- **Проверки:** `SOURCE_RS_TEST_PHYSGUN=1` (спавн арбуза, авто-захват,
|
||
поворот +25°, freeze на 40-м кадре) и `SOURCE_RS_TEST_UNDO=1`
|
||
(`alive 10 → 9`), `SOURCE_RS_TEST_REDO=1` (`9 → 10`). На 35-м кадре виден
|
||
пучок луча и крест на арбузе; на 60-м — жёлтый маркер freeze и `speed=0`.
|
||
Клавиши проверены `xdotool`: `G` спавнит, `Z` откатывает (`depth=0`),
|
||
`Shift+Z` возвращает (`depth=1`), `F` дважды пересоздаёт вьюмодель без
|
||
падения.
|
||
- **Осталось:** звук и эффекты физгана, физические силы/инерция как в GMod
|
||
(сейчас P-регулятор скорости), серверный `PhysgunFreeze`, подсветка цели
|
||
силуэтом, `v_physgun.mdl` в ассетах нет — вьюмодель откатывается на
|
||
пистолет.
|
||
|
||
## 0.11. Q-меню: отзывчивость, слайдеры, колесо и выравнивание (2026-10-24, ветка `feat/menu`)
|
||
|
||
Жалобы пользователя: лаги Q-меню на release, не работают слайдеры, колесо не
|
||
листает списки/дерево, текст «пляшет», часть иконок — заглушки.
|
||
|
||
- **Профиль до.** `SOURCE_RS_UI_PROFILE=1`: снимок Lua пересобирался раз в 6
|
||
кадров, avg 6,4/4,1 мс, max 84/47 мс; 1445 команд; отрисовка 0,17 мс;
|
||
FPS с меню 16,8–17,3 против 20,7–20,9 без меню (Xvfb, замеры шли
|
||
параллельно и мешали друг другу).
|
||
- **Причина.** Дорогой обход всего дерева и `DTileLayout` по тысячам иконок
|
||
каждые 6 кадров; `emit` создавал команды для полностью обрезанных панелей;
|
||
`sort_children` сортировал всех детей сетки; hover на каждом движении мыши
|
||
звал `OnCursorEntered` → `InvalidateLayout`; снимок не знал, изменилось ли
|
||
что-то на самом деле.
|
||
- **Снимок по dirty-флагу.** `ui.mark_dirty(layout)` зовут сеттеры, ввод и
|
||
таймеры; Rust каждый кадр делает `ui_tick()` (таймеры) и `ui_dirty()`, а
|
||
`ui_snapshot_into` — только при изменении. `layout_dirty` отделён от
|
||
отрисовки: hover не пересчитывает геометрию, колесо/перетаскивание — да.
|
||
`paint_panel` не заходит в поддеревья вне clip, `emit` отбрасывает полностью
|
||
невидимые команды, `sort_children` не сортирует панели без ZPos.
|
||
- **Профиль после.** В покое снимок avg 0,26–0,5 мс, max 18–27 мс (редкая
|
||
страховочная пересборка), 677 команд; отрисовка 0,13–0,17 мс. FPS с меню
|
||
18,9/20,5, без меню 17,6/16,9 — разница в пределах разброса. Первый кадр
|
||
после открытия — до ~95 мс (раскладка + первая порция иконок); для этого
|
||
меню прогревается на загрузке карты (`SOURCE_RS_UI_NO_PREWARM=1` отключает).
|
||
- **Слайдеры.** В `VGUI_FILES` добавлен `dnumberscratch.lua`; добавлены
|
||
`NoClipping`, `GetCaretPos/SetCaretPos`, `input.SetCursorPos`. Реальные
|
||
`DSlider`/`DNumSlider` теперь создаются и тянутся: `OnCursorMoved` получает
|
||
локальные координаты, `MouseCapture` удерживает ползунок вне панели. Тест
|
||
`ui_num_slider_drags_and_updates_value` тянет настоящий DNumSlider.
|
||
- **Колесо.** Обработчик всплывает от кнопки/иконки к родителям до
|
||
`DScrollPanel`/`DTree`, пока не вернёт `true`. Починен корень `DTree`:
|
||
`PerformRootNodeLayout` брал высоту `ChildNodes` до `DListLayout`, из-за чего
|
||
canvas был 15 px и VBar выключен; теперь размеры детей считаются раньше.
|
||
Тесты `ui_wheel_scrolls_scroll_panel` и `ui_wheel_scrolls_tree` проверяют
|
||
сдвиг канваса.
|
||
- **Выравнивание.** `surface.GetTextSize` теперь совпадает с bitmap-метрикой
|
||
рендера (8 px × масштаб на байт), вертикаль считается по высоте глифа.
|
||
`ContentAlignment` разобран как в GMod (1–3 низ, 4–6 середина, 7–9 верх;
|
||
внутри ряда лево/центр/право), вкладки центрируются, дерево — слева с
|
||
отступом, заглушка иконки — две строки по центру с обрезкой по иконке.
|
||
- **Иконки.** Очередь видимых моделей сортируется по площади на экране и
|
||
грузится до 8 штук/~8 мс за кадр; кэш LRU переиспользуется при повторном
|
||
открытии. Заглушки остаются только для реально отсутствующих в VFS моделей.
|
||
- **Проверка.** `cargo test -p glua --test ui` (9 тестов, включая перетаскивание
|
||
DNumSlider и колесо над DTree), кадры
|
||
`SOURCE_RS_QMENU=1 ... --frames 90 --screenshot`, клики
|
||
`SOURCE_RS_UI_CLICK='Entities'`, колесо `SOURCE_RS_UI_WHEEL=x,y,d`.
|
||
|
||
---
|
||
|
||
## 0.8. Q-меню Sandbox в клиенте (2026-10-24, ветка `feat/ui-render`)
|
||
|
||
Настоящее меню из `feat/derma` подключено к клиенту; самодельный `qmenu.rs`
|
||
остался только fallback'ом.
|
||
|
||
- `client/src/map.rs`: после `load_scripts` вызывается
|
||
`lua.load_ui(<assets>/garrysmod)`. Признак успеха — живой `g_SpawnMenu`;
|
||
иначе Q открывает встроенное меню (проверка: `SOURCE_RS_NO_LUA_UI=1`).
|
||
`ui_set_screen` вызывается на старте и на resize — мышь не уезжает.
|
||
- Рендер: `client/src/ui_overlay.rs` переводит `UiCommand` в `push_rect`
|
||
(теперь это точный квад, а не растянутый глиф — тонкие линии дерева больше
|
||
не превращаются в квадраты) и `push_text` (scale = h/12). Цвета VGUI
|
||
sRGB→linear, иначе серый 128 на экране становился 188. Бюджет — 10k глифов
|
||
на кадр; команды за пределами экрана отбрасываются.
|
||
- Ввод: Q/Esc управляют меню, Esc больше не выходит из клиента при открытом
|
||
меню. Пока меню открыто, игрок заморожен, ЛКМ не стреляет, мышь уходит в
|
||
`ui_mouse` (`move/down/up/wheel`), колесо не крутит физган. На Android
|
||
четвёртая кнопка MENU (над JUMP) открывает/закрывает меню; касания в меню
|
||
переводятся из нормализованных координат SDL в пиксели и идут в `ui_mouse`.
|
||
- Спавн: `RunConsoleCommand` в Lua теперь кладёт вызовы в очередь
|
||
(`take_console_commands`). Клиент исполняет `gm_spawn` (модель из
|
||
`SpawnIcon.DoClick`), а также `gm_spawnsent`/`gm_spawnswep` через
|
||
`entity_string`/`weapon_string`. Проп появляется на полу перед игроком
|
||
(`qmenu::spawn_origin`) и проходит обычный путь спавна/рендера.
|
||
- Снимок: `ui_snapshot_into` переиспользует буфер, `ui_take_errors` отдаёт
|
||
ошибки Paint/PerformLayout в лог клиента.
|
||
- Отладка кадров: `SOURCE_RS_QMENU=1`, `SOURCE_RS_UI_CLICK=<текст|x,y>`,
|
||
`SOURCE_RS_UI_CLOSE=<кадр>`.
|
||
- Грабли: реальный `DTileLayout` зовёт `ChildrenSize`, `DTree_Node` —
|
||
`HasChildren`, скин — `HasFocus`/`GetTextSize`/`DrawTextEntryText`; без них
|
||
сетка иконок имела высоту 0 и клики не попадали, а дерево схлопывалось.
|
||
Модели по-прежнему без превью (две строки имени), HTML-вкладки — заглушки,
|
||
длинные подписи дерева обрезаются панелью содержимого.
|
||
|
||
---
|
||
|
||
## 0.10. 3D-иконки моделей и светлый скин Q-меню (2026-10-24, ветка `feat/icons`)
|
||
|
||
- Контракт снимка расширен: `UiKind::Image`, `UiCommand.model` (путь MDL),
|
||
`UiCommand.clip` (границы панели). `canvas`/`ModelImage` эмитят `image`
|
||
вместо двух строк имени, `SpawnIcon` работает без изменений.
|
||
- `render::icons::ModelIcons`: offscreen 128×128 (`Rgba8UnormSrgb` + depth),
|
||
камера «три четверти» по габаритам вершин, материалы модели через
|
||
`upload_texture`, LRU-кэш 512 путей, лимиты 400k вершин/24 материала.
|
||
В клиенте — не больше 3 новых иконок за кадр; битые пути кэшируются как
|
||
неудачные. UI-квад рисуется между командами снимка (`ui_image.wgsl`),
|
||
поэтому фон панели остаётся под иконкой, а подписи — сверху.
|
||
- Клиппинг на CPU: `text::build_vertices` обрезает каждый глиф/квад по
|
||
`clip` с пересчётом UV; `paint_panel` сужает clip по дереву, а
|
||
`surface.DisableClipping` снимает его (тени рамок).
|
||
- Светлый скин: `ui.apply_light_skin()` задаёт палитру после
|
||
`skins/default.lua`; `surface.DrawTexturedRectUV` подбирает светло-серый
|
||
цвет по UV gwenskin (панели/кнопки/вкладки/списки), выделение — голубое.
|
||
Раньше крупные белые текстуры скина подменялись тёмным фоном — отсюда была
|
||
тёмная тема.
|
||
- Проверка: `SOURCE_RS_QMENU=1 ... --frames 60 --screenshot` — сетка 3D-иконок
|
||
на светлом фоне; `SOURCE_RS_UI_CLICK=barrel` спавнит `gm_spawn`; после
|
||
`SOURCE_RS_UI_CLOSE` SWEP-атака/переключение работают; FPS-замер
|
||
(`SOURCE_RS_LOG_FPS`) без меню ~14, с меню ~12–13 на Xvfb.
|
||
|
||
---
|
||
|
||
## 0.9. Настоящее Q-меню: Derma + Sandbox spawnmenu (2026-10-24, ветка `feat/derma`)
|
||
|
||
Q-меню больше не рисуется сеткой на Rust: `crates/lua` получил слой VGUI/Derma
|
||
(`ui.rs` + `ui/bootstrap.lua`), поверх которого грузятся настоящие
|
||
`lua/derma/init.lua`, `lua/skins/default.lua`, `lua/vgui/*` и
|
||
`lua/includes/modules/spawnmenu.lua`.
|
||
|
||
- `LuaRuntime::load_ui(game_dir)` собирает UI и геймод, `OnGamemodeLoaded`
|
||
создаёт `g_SpawnMenu`; `ui_toggle/ui_visible/ui_snapshot/ui_mouse` — контракт
|
||
для рендера (`UiKind`, `UiCommand`, лимит 4096 команд/32 уровня).
|
||
- `surface`/`draw` в снимке пишут команды; клик по вкладке/дереву реально
|
||
меняет состояние (`OnMousePressed/Released`).
|
||
- `file.Find` научен GMod-семантике (файлы+каталоги, `*.txt`), добавлен
|
||
`DEFINE_BASECLASS` -> `local BaseClass = baseclass.Get` и `//`-комментарии;
|
||
песочница `include` ищет файлы без учёта регистра (Windows-скрипты на Linux).
|
||
- Снимок на реальных ассетах: ~900 команд, вкладки/категории/модели
|
||
(`Construction Props`, `*.mdl`), клик по Weapons меняет снимок.
|
||
- Ограничения: HTML-вкладки (Dupes/Saves) — заглушки, модели не рендерятся
|
||
(только имена), шрифты приблизительные; `crates/client` не тронут.
|
||
|
||
---
|
||
|
||
## 0. SWEP: оружие из Lua в клиенте (2026-10-24, ветка `feat/swep`)
|
||
|
||
Скрипты `lua/weapons/*.lua` (на реальных ассетах — `weapon_fists`,
|
||
`weapon_flechettegun`, `weapon_medkit`) теперь не просто загружаются, а
|
||
**управляют клиентом**:
|
||
|
||
- `client/src/weapon.rs`: реестр из `list.Get("Weapon")`, фильтр
|
||
`Spawnable` + существующий `ViewModel` в VFS, слоты (`Slot`/`SlotPos`),
|
||
`SOURCE_RS_SWEP` для старта. Клавиши `1..9` выбирают слот 1..9, `0` — слот 0
|
||
(кулаки). При смене вызываются `Holster` старого и `Deploy` нового,
|
||
вьюмодель пересоздаётся под `SWEP.ViewModel`.
|
||
- `client/src/map.rs`: `ViewModelState` (модель, покой/выстрел, пересоздание),
|
||
ЛКМ → `PrimaryAttack` активного SWEP, автоматический огонь по
|
||
`Primary.Automatic` с учётом `GetNextPrimaryFire`, `Think` каждый кадр,
|
||
HUD с `PrintName` и патронами (`Clip1`/`ClipSize`).
|
||
- `glua`: база SWEP (`swep_base.lua`), `GetOwner`/`GetViewModel`, `NetworkVar`,
|
||
`SetNextPrimaryFire`, `Clip1`, `EmitSound`; глобальные `CurTime`, `IsValid`,
|
||
`player.GetAll`, `util.TraceLine`/`TraceHull`, `trace.*`, `sound.*`;
|
||
`game.SinglePlayer` исправлен на **функцию** (как в GMod), `GetConVar` знает
|
||
`sv_defaultdeployspeed`/`phys_pushscale` (ноль давал `inf` в `Deploy`).
|
||
- Ошибка Lua в любом методе логируется и не роняет кадр; `Think` после ошибки
|
||
отключается для этого оружия.
|
||
|
||
**Проверка (Xvfb, `--hidden`):** `SOURCE_RS_SWEP=weapon_*` +
|
||
`SOURCE_RS_TEST_ATTACK=1` (ЛКМ на 10-м кадре) и `SOURCE_RS_TEST_SWITCH=1`
|
||
(смена оружия каждые 10 кадров). Flechette за 10 кадров сделал 5 выстрелов
|
||
(`SetNextPrimaryFire` = 0.1), ошибок Lua нет. Кадры:
|
||
`/tmp/opencode/swep_fists.png`, `swep_flechette.png`, `swep_medkit.png`,
|
||
`swep_switch.png`. Тесты: `real_gmod_weapon_methods_run_without_errors`
|
||
прогоняет настоящие Deploy/PrimaryAttack/Think/Holster всех трёх SWEP.
|
||
|
||
**Грабли:** `c_arms.mdl` — скелет без геометрии (`c_arms.vvd` = 0 вершин), руки
|
||
в GMod берутся из модели игрока. Пока bonemerge нет, вьюмодель кулаков
|
||
скрывается (HUD показывает оружие), а не подменяется пистолетом. `c_smg1` и
|
||
`c_medkit` рисуются, но общая проекция камеры (FOV 100, дистанция 26) делает их
|
||
мелкими: отдельный FOV/масштаб вьюмодели — следующий шаг.
|
||
|
||
---
|
||
|
||
## 0. HDR и попиксельный свет (2026-10-23, ветка `feat/hdr`)
|
||
|
||
Сделано по жалобе «свет резко идёт по полигонам» и задаче HDR:
|
||
|
||
- **A10, попиксельная лайтмапа.** `LUMP_LIGHTING` теперь раскладывается в атлас
|
||
`Rgba16Float` (полочная упаковка, рамка 1 тексель на блок), у вершины появился
|
||
`lightmap_uv`, шейдер мира читает свет на каждый пиксель с билинейной
|
||
фильтрацией. На gm_construct: 6891 грань, 751 718 люкселей, атлас 2048²
|
||
(32 МиБ). Если атлас не собрался — пайплайн с `fs_main_vertex`, вершинный свет.
|
||
- **HDR.** Мир, небо и вода рисуются в `Rgba16Float`, затем композит:
|
||
экспозиция → ACES → bloom (порог яркости в ½ разрешения, два прохода
|
||
«горизонт+вертикаль», аддитивно). UI/диагностика — отдельным проходом уже в
|
||
sRGB-поверхность, как раньше.
|
||
- **Тени.** PCF 9 отсчётов (центр + два кольца) с поворотом по мировой позиции и
|
||
нормальным смещением; `SOURCE_RS_DEBUG_SHADOW=1` выводит коэффициент тени —
|
||
по кадру видно мягкий край и отсутствие acne.
|
||
- **Переключатели:** `SOURCE_RS_NO_HDR=1` (старый путь), `SOURCE_RS_EXPOSURE`
|
||
(1.0), `SOURCE_RS_BLOOM` (0.5), `SOURCE_RS_BLOOM_THRESHOLD` (1.0),
|
||
`SOURCE_RS_DEBUG_SHADOW`, `SOURCE_RS_NO_VSYNC`, `SOURCE_RS_LOG_FPS=1`.
|
||
- **Замер** (Xvfb, 1280×720, камера E11 `-399.5 -1604.5 67.9`, 6 окон по 120
|
||
кадров без vsync, A/B на одном и том же бинарнике и на коммите до правок):
|
||
до правок 23,6–26,5 fps (в среднем ~24,5), после с HDR 22,1–26,4 (~23,7),
|
||
при `SOURCE_RS_NO_HDR=1` 23,0–25,7 (~24,4). Разница в пределах разброса
|
||
прогонов; на 320×180 та же сцена даёт 87–108 fps. 60 fps на 1280×720 в
|
||
Xvfb не было и до правок — узкое место ~2661 draw call на проход (плюс
|
||
столько же в теневом), а не HDR/лайтмапа; это следующий шаг.
|
||
- **Кадры:** `/tmp/opencode/hdr_before_tunnel.png` → `hdr_final_tunnel.png`
|
||
(градиент на кирпичной стене вместо плоского света), `hdr_final_e11.png` и
|
||
`hdr_final_e11_nohdr.png` (HDR/старый путь), `hdr_shadowdbg_e11.png` (PCF).
|
||
- **Тесты:** `lightmap_atlas_maps_face_block`, `half_conversion_known_values`
|
||
(+ все прежние сьюты зелёные).
|
||
|
||
---
|
||
|
||
## 0.1. Итог сессии 2026-10-23: все три ветки слиты
|
||
|
||
- `feat/anim` (d42d646) — RAWROT/RAWROT2/RAWPOS, выбор анимации по состоянию,
|
||
покачивание вьюмодели и `fire`. Слито `3de8b89`.
|
||
- `feat/android` (b28d6f9) — APK под Android 12+ (arm64, minSdk 31), SDL3 из
|
||
исходников, сенсорное управление, сборка без Gradle (`android/build.sh`).
|
||
Слито `2edea01`; пакет `target/android/gmod-rs-debug.apk`.
|
||
- `feat/hdr` (dec8b67) — попиксельная лайтмапа в атласе Rgba16Float, HDR
|
||
(ACES + bloom), PCF-тени. Слито `1f42bb9`.
|
||
|
||
Перед слиянием в `main` также вошли: коллизия пропов из настоящего меша
|
||
(выпуклая оболочка для динамических, тримеш для статических — `a491010`) и
|
||
вьюмодель в камерном базисе с доворотом в координатах модели (`a491010`).
|
||
|
||
Состояние: 25 сьютов, 29 картовых тестов, 15 тестов world_real — зелёные;
|
||
clippy чистый.
|
||
|
||
## 0.2. Проверка Android-сборки после слияния
|
||
|
||
Пересобраны оба APK (`android/build.sh`): arm64 — `target/android/gmod-rs-debug.apk`
|
||
(6,3 МБ, sha256 `e029c330…`), x86_64 — `gmod-rs-debug-x86_64.apk`.
|
||
|
||
На эмуляторе API 35 (SwiftShader) установлен x86_64-вариант: `gm_construct`
|
||
загружается, HUD и сенсорные подписи на месте, FPS 8–18 (программный рендер),
|
||
вьюмодель рисуется. Пурпурные текстуры на эмуляторе — отсутствующие материалы
|
||
HL2: туда залит только `garrysmod/`, без `sourceengine/`. Проверено на десктопе
|
||
с тем же неполным набором ассетов — картина идентична (пурпурный пистолет),
|
||
значит на телефоне с полным `server/` всё будет текстурировано.
|
||
|
||
Важно: `adb uninstall` чистит `/sdcard/Android/data/<пакет>/files` — после
|
||
переустановки ассеты надо заливать заново.
|
||
|
||
## 0.3. Размер APK и встроенные ассеты
|
||
|
||
Замер `fs pack` для gm_construct: 335 файлов из VPK (27,0 МБ: 277 материалов,
|
||
23 модели, 35 Lua) + карта 35,0 МБ = **62,1 МБ** сырых, 31 МБ в tar.gz.
|
||
Собранный APK со встроенным набором — **38 МБ** (83409194 байта в распаковке,
|
||
344 файла) против 6,3 МБ без ассетов. `MainActivity` распаковывает
|
||
`assets/gmod` при первом запуске; `adb push` не нужен. Каталог `android/assets/`
|
||
не коммитится.
|
||
|
||
Проверено на эмуляторе API 35 **с чистого листа** (uninstall → install APK
|
||
38 МБ → запуск): распаковка заняла 1,24 с, `детект` ассетов прошёл, мир собран
|
||
с `missing=0`, карта и пистолет полностью текстурированы, FPS 9 на SwiftShader.
|
||
|
||
Грабли: `tier::paths::detect` признаёт корень по `garrysmod_dir.vpk` или
|
||
`garrysmod/gameinfo.txt` — без маркера клиент говорил «ассеты не найдены»,
|
||
хотя файлы на месте; `fs pack` теперь пишет `gameinfo.txt`.
|
||
|
||
## 0.4. Цветокор, зеркало, освещение — что выяснено
|
||
|
||
- **Цветокор** реализован: LUT карты читается из
|
||
`materials/colorcorrection/<карта>.raw` (32³ RGB, 98 304 байта), грузится в
|
||
3D-текстуру Rgba8Unorm и применяется в композите после ACES (sRGB→LUT→sRGB).
|
||
Замер: файл `gm_construct.raw` **нейтральный** (диагональ 0/66/132/195/255),
|
||
то есть у gm_construct грейда нет by design; другие карты подхватятся сами.
|
||
Отключение: `SOURCE_RS_NO_COLORCORRECTION=1`.
|
||
- **Зеркало** на карте есть: `func_reflective_glass` (1 шт.) — это кисть с
|
||
реальным отражением в Source. Нужен отдельный проход: отражённая камера →
|
||
текстура → грань зеркала с экранными UV. Задача записана, не начата.
|
||
- **Освещение моделей**: вершины моделей получают `lightmap_uv` белого текселя
|
||
атласа, то есть ambient карты + солнце с тенями + точечные источники. Чего
|
||
нет — запечённого ambient **в точке модели** (тёмный угол не затемняет проп).
|
||
План: при спавне искать ближайшую грань BSP и брать средний цвет её блока
|
||
лайтмапы как `light` модели.
|
||
- Прочее из «визуального потенциала»: `infodecal` ×19 (декали не рисуются),
|
||
`env_sprite`/glow нет, `env_skypaint` не читается (небо из sky-материалов),
|
||
`env_tonemap_controller` не используется (экспозиция своя).
|
||
|
||
## 0.5. Зеркало (`func_reflective_glass`) — сделано
|
||
|
||
- в сборке мира модель зеркала помечается материалом-меткой `<mirror>`
|
||
(грани рисуются отдельным конвейером); у `reflectiveglass001` в игре нет
|
||
VTF, поэтому пометка ставится **до** резолва материала, иначе грань
|
||
выпадала из мира;
|
||
- из граней модели строится плоскость (`MirrorPlane { origin, normal }`):
|
||
у gm_construct — точка (-2064, -2067, -286), нормаль (0,1,0);
|
||
- рендер: отдельный проход отражённой камерой в текстуру Rgba16Float, клип
|
||
по плоскости зеркала (плоскость едет в uniform камеры, `discard` в шейдере);
|
||
грань зеркала сэмплирует отражение по экранным координатам. Группа 4 для
|
||
отражения не подошла: `max_bind_groups = 4`, поэтому текстура отражения
|
||
биндится на слот лайтмапы (группа 3) в отдельном bind group;
|
||
- зеркало рисуется после непрозрачного мира (обычный проход пропускает
|
||
зеркальные батчи, иначе чекер перекрывал отражение), тени от зеркала не
|
||
считаются.
|
||
|
||
Проверено кадрами: UV-градиент на грани показал, что зеркальная плоскость
|
||
занимает всю стену и шейдер работает; отражение показывает противоположную
|
||
кирпичную стену и потолок. Дальше — сглаживание границы отражения и
|
||
исключение зеркала из прохода теней.
|
||
|
||
## 0.6. Декали (infodecal) — сделано
|
||
|
||
19 декалей карты (граффити, плакаты, следы шин) ставятся на ближайшую грань:
|
||
замер показал расстояние 0 у всех, поэтому проекция точная; квадрат 32×32
|
||
строится по осям текстуры грани. Материалы `decals/...` рендерятся отдельным
|
||
проходом с альфа-блендингом (шейдер `fs_decal`: цвет из общего освещения,
|
||
альфа из текстуры). Проверено кадром: граффити с прозрачностью на кирпиче.
|
||
|
||
Осталось из визуального: `env_skypaint` (цвет неба), спрайты/glow (на
|
||
gm_construct их нет), SWEP из Lua.
|
||
|
||
## 0.7. Q-меню спавна (2026-10-24, ветка `feat/qmenu`)
|
||
|
||
Сделано ядро A24 — меню спавна по клавише Q, без VGUI:
|
||
|
||
- **Список** собирается при старте: `list.Get("SpawnableEntities")` (только
|
||
`Spawnable = true`), `list.Get("Weapon")` (`WorldModel`, иначе `ViewModel`) и
|
||
модели из `settings/spawnlist_default/*.txt` (KeyValues). Все пути проверяются
|
||
`vfs.exists`; на текущих ассетах: 1 энтити, 3 оружия, 5656 пропов (1817 путей
|
||
из spawnlist отсутствуют и отброшены). Модель ENT-скрипта, заданная только в
|
||
коде (`self:SetModel`), берётся первым литералом `models/*.mdl` из исходника.
|
||
- **UI** — текст поверх затемнения: вкладки 1/2/3, сетка 4×8, стрелки/Enter,
|
||
мышь (наведение и клик), колесо и PageUp/PageDown, Esc/Q закрывают. Пока меню
|
||
открыто, игрок не ходит, камера не вращается, ЛКМ не берёт физган; после
|
||
закрытия управление и захват мыши возвращаются.
|
||
- **Спавн** — в точку перед игроком: горизонтальный луч с габаритом пропа,
|
||
затем луч вниз до пола; если пола нет — на уровне глаз. Внутрь игрока не
|
||
ставим. Путь спавна общий с G (`spawn_prop` + досборка рендерера).
|
||
- **Проверки:** клик/Enter спавнят (кадр `interactive2.png`), G по-прежнему
|
||
спавнит (`gspawn.png`), при открытом меню позиция не меняется (`frozen.png`),
|
||
после закрытия W снова двигает (`resume.png`); скриншоты меню — `final_menu.png`,
|
||
`menu_entity.png`, `menu_weapon.png`, спавн арбуза — `final_spawn.png`.
|
||
- **Отладка:** `SOURCE_RS_QMENU=1` — открыть меню на старте;
|
||
`SOURCE_RS_QMENU_ITEM=<подстрока>` — выбрать предмет; `SOURCE_RS_QMENU_SPAWN=1`
|
||
— заспавнить выбранный сразу (проверка кадром).
|
||
- **Тесты:** `qmenu` (навигация, страницы, попадание мышью, категории),
|
||
`glua` (поля ENT-реестра, поиск литерала модели), `render` (сплошной глиф).
|
||
|
||
Осталось: иконки-превью вместо текста, подкатегории spawnlist (сейчас один
|
||
общий список пропов), серверный спавн настоящих ENT вместо пропа-представителя.
|
||
|
||
## 1. Что просит пользователь (приоритеты)
|
||
|
||
**Ускоренный путь до Sandbox-UX** (его слова): «быстрее дойти до того, как смогу
|
||
спавнить через Q-меню, иметь норм модель игрока, таскать физганом, Weapon всё
|
||
сделать». Поэтому очередь переставлена (docs/PARITY.md A25):
|
||
|
||
1. A23 оружие (SWEP из Lua + вьюмодель + аттачменты) — сюда же A20 (модель
|
||
игрока: один и тот же рендер моделей).
|
||
2. A21 физика взаимодействия: физган (луч → проп → таскание, вращение,
|
||
заморозка), без полной гибернации.
|
||
3. A24 спавн: сначала консольные команды на данных из Lua (`gm_spawn`,
|
||
`gm_give`, режимы `gmod_tool`), потом окно Q.
|
||
4. A22 Sandbox целиком: undo/cleanup/duplicator.
|
||
5. Визуальная точность (A10 свет на пиксель, A11–A12 вода, A16 трава/декали,
|
||
A18 скайбокс) — параллельно, Sandbox не блокирует.
|
||
|
||
## 2. Где я сейчас (в работе)
|
||
|
||
**A20/A23, первый срез сделан: модель игрока в мире.** Рендер тот же, что у
|
||
пропов (статический меш `PropMesh`), скелет и анимации — следующий срез.
|
||
|
||
Как включается и как проверять:
|
||
|
||
```bash
|
||
SOURCE_RS_PLAYER_MODEL= DISPLAY=:99 SDL_VIDEODRIVER=x11 cargo run -q -p client -- \
|
||
map --assets ~/gmod_server/server gm_construct --hidden --frames 4 --noclip \
|
||
--player-pos="-2438 -966 48" --flythrough /tmp/opencode/pl4 --flythrough-frames 4 \
|
||
--route="-2438 -880 75|6 270; -2438 -1180 75|6 90"
|
||
```
|
||
|
||
Пустое значение — модель по умолчанию (`models/player/police.mdl`), иначе —
|
||
путь к модели. Курс разворачивается на 180° (у моделей Source ось Y влево).
|
||
|
||
Движок: `World::spawn_model_instance` и `World::move_model_instance`,
|
||
`ModelInstance` (экземпляр без сущности). Грабли: индексы модели **локальные** —
|
||
при вставке в общий буфер к ним обязательно прибавляется `first_vertex`
|
||
(без этого был мусор в кадре).
|
||
|
||
**Спавн пропа из игры сделан (ядро A24).** `World::spawn_prop` (геометрия из
|
||
`append_model_geometry`, сущность `prop_physics`, тело в физике, экземпляр
|
||
пропа) + в клиенте клавиша `G` (спавн по взгляду) и аргумент
|
||
`--spawn <модель>` (отладка и скрипты). Проверено кадром: арбуз заспавнен и
|
||
отрисован (`--spawn models/props_junk/watermelon01.mdl`, камера у
|
||
`-2500 -1050 60|5 0`).
|
||
|
||
Грабли: буферы рендера созданы на сборке мира — после спавна рендерер
|
||
пересобирается целиком (медленно, но работает); позже нужна до-аллокация
|
||
вершин под спавн.
|
||
|
||
**Физган, первый срез (A21/A23).** ЛКМ — взять ближайшее тело под прицелом
|
||
(луч из глаз, расстояние 8..260 юнитов, допуск 40 в стороны), пока держим —
|
||
тело каждый кадр телепортируется на луч взгляда на взятом расстоянии; отпустил —
|
||
свободно. `SOURCE_RS_PHYSGUN=1` держит ближайшее тело без мыши (для скриптов).
|
||
|
||
**Нужна интерактивная проверка физгана:** мои скриптовые кадры упираются в
|
||
стены — камеру без глаз не навести. Проверять так: встать у кучи пропов
|
||
(она в районе `-2930 -1370 -64`), посмотреть на проп, зажать ЛКМ.
|
||
|
||
**Синхронизация физики и картинки (жалоба «позиция рендера обновляется
|
||
только при спавне нового»).** Причина найдена: мир двигал вершины в памяти, а
|
||
буфер рендера заливался один раз при сборке — потому спавн (пересборка
|
||
рендерера) и «лечил». Сделано: `World::moved_ranges` + `take_moved_ranges()`
|
||
(диапазоны заполняет `rewrite_instance`), `WorldRenderer::upload_vertex_ranges`
|
||
(точечная заливка `queue.write_buffer`) и вызов в кадре клиента.
|
||
|
||
**Не сходится (диагностика `probe_moving_prop_reports_vertex_ranges`):**
|
||
коробка в тесте задана в мировых координатах, а меш обязан быть в локальных —
|
||
видно по двойному смещению вершины; плюс список диапазонов почему-то пуст после
|
||
`step_physics` (проверить `is_sleeping` и ранний выход по совпадению позы —
|
||
вероятно, тело «спит» после телепорта). Проверять в интерактиве: заспавнить
|
||
арбуз, посмотреть, что он падает на экране.
|
||
|
||
**Найдены и закрыты две поломки по жалобе «физика висит на экране»:**
|
||
|
||
1. `move_prop` остался со своей копией перезаписи вершин и не запоминал
|
||
диапазоны — теперь он идёт через `rewrite_instance` (замер: было «диапазонов 0»
|
||
каждый шаг, стало «диапазонов 1 [(39585, 8)]»).
|
||
2. Буфер вершин мира создавался **без `COPY_DST`** — заливка падала с ошибкой
|
||
wgpu (у пользователя это роняло клиент). Теперь `VERTEX | COPY_DST`, для
|
||
индексов `INDEX | COPY_DST` плюс `upload_index_ranges`.
|
||
|
||
Плюс `WorldRenderer::refresh_batches`: рендерер копировал список батчей при
|
||
сборке, поэтому **новая** геометрия (спавн) в кадр не попадала без полной
|
||
пересборки. Теперь список обновляется, когда появились новые индексы, а полная
|
||
пересборка нужна только при новой текстуре.
|
||
|
||
**Открытый вопрос: вьюмодель.** Создание работает (модель в буфере, вершины
|
||
+1000, батч есть, габарит ±15), но в кадре её нет — при этом модель игрока
|
||
рисуется. Разница в том, что модель игрока в том тесте не двигалась (кадры
|
||
пролёта), а вьюмодель обязана двигаться каждый кадр. Значит сломано именно
|
||
звено «перенос позы каждый кадр + заливка на GPU». Включается
|
||
`SOURCE_RS_VIEW_MODEL` (по умолчанию физган, есть откат на монтировку —
|
||
`v_physgun.mdl` в ассетах не нашёлся, грузится `v_crowbar.mdl`).
|
||
|
||
**Вьюмодель: СДЕЛАНО (первый срез).** Механизм проверен кадром: пистолет
|
||
(`SOURCE_RS_VIEW_MODEL=models/weapons/v_pistol.mdl`) стоит в кадре. Две причины,
|
||
почему раньше не рисовалась: (1) разворот на 180° уводил ствол за камеру —
|
||
у моделей оружия Source геометрия вынесена вперёд по оси модели; (2) у моделей
|
||
оружия геометрия смещена от начала координат (у пистолета центр габарита
|
||
`≈(0,0,47)`, у монтировки `−52` по Y), поэтому привязываться надо к **центру
|
||
габарита** модели (`engine::model::rotate_model_vector(centre, ...)`), а не к её
|
||
origin. Посадка: глаз + 20 вперёд + 6 вправо − 10 вниз − центр.
|
||
|
||
Что осталось по вьюмодели: `v_physgun.mdl` в ассетах нет (GMod-модель), поэтому
|
||
по умолчанию пистолет; отдельный FOV вьюмодели и анимации — позже.
|
||
|
||
**Старые факты (сохранены):** Материал вьюмодели в мире —
|
||
индекс 54, имя `crowbar_cyl` (виден в `--debug-materials`). Рендер одним этим
|
||
материалом (`--only-material 54`) даёт **пустой кадр** — то есть дело не в позе:
|
||
батч вьюмодели не доходит до отрисовки. Проверять по порядку: (1) попал ли батч
|
||
в `renderer.batches` после `refresh_batches` (он копирует список только когда
|
||
`moved_index_ranges` непуст, а они забираются в блоке физики — возможно,
|
||
диапазоны берутся ДО того, как `refresh_batches` увидит их); (2) совпадает ли
|
||
`batch.material` с позицией материала в `world.materials` (дедупликация по
|
||
имени); (3) `--only-material` фильтрует батчи, но не мешает ли ему что-то ещё.
|
||
|
||
**2026-10-23, сессия по жалобам пользователя.** Закрыто коммитом `f58eafe`:
|
||
|
||
- падение wgpu на втором спавне («Copy at offset … overrunning the bounds»):
|
||
буферы GPU не росли — добавлены `ensure_capacity` (рост вдвое с перезаливкой)
|
||
и `refresh_materials` (bind group для новых материалов, иначе новые батчи
|
||
молча не рисовались);
|
||
- вьюмодель: обновляется после `player.update` и `camera.pos` (раньше
|
||
отставала на кадр — «плывёт при повороте»), по умолчанию `w_pistol.mdl`,
|
||
курс −90° (у модели ствол вдоль +Y, а не +X), дистанция 26, анимация `idle01`;
|
||
- `bone_world_transforms` обходит кости рекурсивно (порядок в файле не
|
||
гарантирован);
|
||
- HUD: отладочные строки (verts/tris, лог Lua) скрыты по умолчанию, клавиша `.
|
||
|
||
**Коллизия пропов (2026-10-23).** Коллайдер-коробка ставилась от **нуля
|
||
модели**, хотя геометрия часто смещена (у куклы центр AABB на 13,5 юнита выше
|
||
нуля) — пропы висели на невидимых выступах. Добавлен
|
||
`Physics::add_prop_offset` (смещение коллайдера в координатах модели,
|
||
поворачивается вместе с телом), оба места создания тел (пропы карты и спавн)
|
||
передают `bounds_center`. Коробки пропов для коллизии игрока теперь тоже
|
||
учитывают центр и поворот (`World::prop_collision_boxes`, AABB описанной
|
||
коробки по модулю матрицы поворота).
|
||
|
||
Замер: тест `real_map_props_rest_on_ground` — после 10 с физики низ коробки
|
||
каждого пропа лежит на опоре (допуск 4 юнита, луч от центра вниз); кукла
|
||
остаётся с зазором 10 юнитов, потому что лежит краем на столе — центровой луч
|
||
опору не видит.
|
||
|
||
**Известная проблема:** `v_pistol.mdl` рисуется плоским «листом» (2157 вершин,
|
||
2 меша, 1 материал, скиннинг при этом цел — тест `real_view_model_skin_stays_coherent`
|
||
даёт максимальный сдвиг 4 юнита). Похоже на проблему загрузки геометрии именно
|
||
этой модели; до выяснения по умолчанию стоит `w_pistol.mdl`.
|
||
|
||
**Запущены параллельно (воркдеревья от `f58eafe`):** `feat/android` — APK под
|
||
Android 12+; `feat/hdr` — HDR, тонмаппинг, bloom и мягкие тени/свет; `feat/anim` —
|
||
RAWROT/RAWPOS/RAWROT2, выбор анимации по состоянию (идёт/бежит/приседает/
|
||
прыжок), покачивание вьюмодели.
|
||
|
||
**A26, состояние на 2026-10-17:** сделаны секвенции (с определением шага записи
|
||
по данным), кости и веса вершин, слияние включённых моделей (у игрока 465
|
||
секвенций), чтение `.ani` (7,1 МБ, 1563 анимации, 247 блоков), декодер кадров
|
||
(`mstudioanim_t`, проверен: 280 кадров, 51 дорожка), сборка кватерниона с
|
||
нормировкой 1/32768 (выбрана замером: умножение на rot_scale давало 1,63 —
|
||
для кватерниона невозможно) и CPU-скиннинг (виден кадром).
|
||
|
||
**Следующий шаг — проигрывание.** План, чтобы не искать заново:
|
||
1. `PropMesh` (или `ModelInstance`) должен держать **данные анимации**:
|
||
`.ani` (7 МБ) + список блоков + описания анимаций, ссылкой `Arc`, — сейчас
|
||
они читаются при загрузке модели и выбрасываются;
|
||
2. в движке `pose_model_instance(index, origin, angles, bone_quats: &[[f32;4]])`:
|
||
локальные кватернионы костей в мировые через родителей
|
||
(`world[i] = world[parent] · (T(pos_i) · R(q_i) · T(−pos_i))`), затем скиннинг
|
||
теми же весами (функция скиннинга уже есть — сейчас её ведёт синус);
|
||
3. в клиенте: выбрать секвенцию (`@idle_all_01` через последовательность
|
||
activity), взять кадр по времени (`t · fps`, циклом), собрать кватернионы
|
||
костей из дорожек и позвать `pose_model_instance`;
|
||
4. затем выбор по состоянию (`ACT_MP_*`) и блендинг, и снять синус.
|
||
|
||
**A26 «анимации» — план записан** (docs/PARITY.md A26). Ключевое открытие: в
|
||
`formats` скелет и веса **уже читаются** (`mdl.bones()`, `vvd.vertices()`,
|
||
`vtx.body_parts()`), не хватает разбора секвенций/`.ani`, костей в
|
||
`ModelVertex` и CPU-скиннинга. Заливка вершин на GPU для этого уже готова —
|
||
тот же путь `moved_ranges`, что у движущихся пропов.
|
||
|
||
**A26, сделано 2026-10-23 (этот заход):** постоянные дорожки
|
||
`RAWROT`/`RAWROT2`/`RAWPOS` разобраны по `compressed_vector.h` (Quaternion48 6
|
||
байт, Quaternion64 8, Vector48 — три half-float). Замеры: `v_pistol @idle01` —
|
||
10 дорожек, 7 RAWROT + 2 RAWPOS; `m_anm @taunt_muscle_base` — 53 дорожки, 24
|
||
RAWROT2, кватернионы единичные, неизвестных флагов нет. `@taunt` теперь
|
||
двигает 3145 из 3147 вершин между кадрами 0 и 30.
|
||
|
||
Выбор анимации по состоянию — `engine::animation`: `idle_all_01`, `a_WalkN`,
|
||
`a_RunN`, `@cidle_all`, `a_CwalkN`, `@jump_fist` (первая существующая), восемь
|
||
направлений относительно взгляда. Фаза walk↔run сохраняется, смена типа —
|
||
сброс. Клиент играет выбранное имя; вьюмодель качается при ходьбе и отстаёт
|
||
при повороте, ЛКМ без захвата проигрывает `fire`. Проверки:
|
||
`real_animation_raw_flags_decode`, `real_player_taunt_moves_vertices_with_raw_tracks`,
|
||
юнит-тесты `engine::animation`.
|
||
|
||
**Следующее:** (а) до-аллокация вершин под спавн вместо пересборки рендерера;
|
||
(б) блендинг анимаций игрока и вьюмодели; (в) вращение пропа колесом и
|
||
заморозка у физгана; (г) оружие в руках связывает `@idle_*`/`@jump_*` и
|
||
вьюмодель; (д) консольные команды спавна на данных из Lua (`gm_spawn`).
|
||
|
||
## 3. Что уже сделано и лежит в `main`
|
||
|
||
Последние коммиты (свежие сверху):
|
||
|
||
- `4ad0405` очередь: A23 оружие, A24 Q-меню, A25 ускоренный порядок;
|
||
- `7a8a750` очередь: A20 модель игрока, A21 полная физика, A22 Sandbox через GLua;
|
||
- `257ac07`, `d35efef` **A13 туман сделан** целиком (чтение + шейдер);
|
||
- `72bad60` A19: подшаг не гасит скорость о склон (35°: 65 → 147);
|
||
- `5bdbdbf`, `e638607` A19: замер по углам + найден `flRedirectCoeff` в движке;
|
||
- `3bfd078`, `d88988f` рельеф: треугольники отсекают шаг, нормаль наружу (стены
|
||
по рёбрам полигонов ушли);
|
||
- `b74824b` склон: догон земли трассировкой (рампы 242–250 вместо 197,8);
|
||
- `23c5034` присед: 250/150/520/216,7 по спеке, камера 46 → 28 (была выше
|
||
хитбокса);
|
||
- `aa11713`, `d220b5a` рампы и стыки: отсечение по последней входной грани,
|
||
касание ребром не преграда;
|
||
- `86a6cb6`, `5380184` инструменты: слой коллизий, дамп кирпича, обход карты.
|
||
|
||
Все 23 сьюта и 28 картовых тестов зелёные.
|
||
|
||
## 4. Замеры, которые нельзя потерять
|
||
|
||
- **A19 (скорость на склоне)**: замер `probe_slope_angles` (синтетический клин).
|
||
Было: 5° 239, 10° 232, 15° 196, 20° 152, 25° 116, 30° 88, 35° **65**, 40° 49,
|
||
44° 39. Стало: 239 / 233 / 226 / 214 / 195 / 175 / **147** / 132 / 107.
|
||
Цель — 250 до 44°. Остаток теряется на прямом (скользящем) пути: иногда он
|
||
выигрывает выбор у подшага. Инструмент: `SOURCE_RS_TRACE_STEP=1` печатает,
|
||
какой путь победил (`step_move`).
|
||
- **Механизм движка для скорости на склоне**: `gamemovement.cpp`, `ClipVelocity`
|
||
(строка ~3144) — блок `flRedirectCoeff`: `out *= (len + |v·n|·coeff)/len`.
|
||
Наивный перенос копит бонус за кадр (15° → 442 юнита/с) — откачен.
|
||
- **Туман карты gm_construct**: цвет (0,557 0,643 0,698), start 1024, end
|
||
250000, max_density 1,0, farz −1 (дальний, на ближних дистанциях нейтрален).
|
||
- **Габариты/линейки Source**: тело 72/36 (стоя/присед), глаза 64/28
|
||
(`VEC_VIEW`/`VEC_DUCK_VIEW`), `sv_walkable_normal` 0,7 (45,6°), `DIST_EPSILON`
|
||
1/32, `sv_stepsize` 18, воздушный потолок желаемой скорости 30.
|
||
- **Коллизия gm_construct**: 2118 кирпичей; 140 за габаритами карты (3D-скайбокс,
|
||
часть с несогласованными плоскостями — см. E19.2); 11776 треугольника рельефа,
|
||
у всех намотка «гео внутрь» (нормаль разворачиваем в `clip_triangle`).
|
||
- **Присед**: обрезка `HandleDuckingSpeedCrop` = 0.33333333 только в приседе и
|
||
на земле, желаемая скорость 450 (бег 650), максимум 250 / 520.
|
||
|
||
## 5. Инструменты диагностики (не переизобретать)
|
||
|
||
- `SOURCE_RS_DEBUG_COLLISION=<радиус>` — слой коллизий: красные рёбра кирпичей,
|
||
жёлтые габариты, голубой рельеф, зелёное тело.
|
||
- `probe_brush_dump` (`SOURCE_RS_TEST_BSP_AT="x,y,z,радиус"`) — полный дамп
|
||
кирпича: содержимое с расшифровкой, владелец-сущность, каждая грань с
|
||
текстурой, габарит, попадание в коллизию.
|
||
- `probe_ramp_movement` — движение из точки: 8 направлений, рывки, залипания,
|
||
кто мешает (кирпич + текстуры).
|
||
- `probe_slanted_surface` — лучи по наклонным кирпичам против настоящей формы.
|
||
- `probe_slope_angles` — таблица скорости по углам (приёмка A19).
|
||
- `probe_phantom_walls_scan`, `probe_collision_contents` — обход карты и состав
|
||
коллизии.
|
||
- `real_map_*` — 28 картовых тестов, требуют `SOURCE_RS_TEST_BSP`.
|
||
- Запуск клиента: `DISPLAY=:99 SDL_VIDEODRIVER=x11 cargo run -q -p client --
|
||
map --assets ~/gmod_server/server gm_construct ...`, для кадров нужен
|
||
`--flythrough <dir>`.
|
||
|
||
## 6. Грабли (уже наступал)
|
||
|
||
- `Vec3` без `Index`: писать покомпонентно.
|
||
- `%[fx:mean*255]` в ImageMagick без имени канала врёт — считать
|
||
`mean.r/g/b`.
|
||
- Тесты с картой имеют свой `VirtualFs::new()` (189 материалов) — индексы
|
||
материалов не как в рантайме (54).
|
||
- Правки кода делать `edit`, не sed/python, кроме больших перестановок блоков —
|
||
и после каждой проверять `cargo clippy` и тесты.
|
||
- Перед коммитом всегда: `cargo fmt --all && cargo clippy --workspace
|
||
--all-targets -- -D warnings && cargo test --workspace` + картовые тесты.
|