# Дневник агента: где я и что дальше Живой документ. Обновляю по ходу работы, чтобы после автоматического уплотнения контекста ничего не терялось. Коротко: состояние, замеры, следующий шаг, грабли. Обновлён: 2026-10-16, вечер. --- ## 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`). **Вьюмодель: новые факты (для продолжения).** Материал вьюмодели в мире — индекс 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` фильтрует батчи, но не мешает ли ему что-то ещё. **Следующее:** (а) до-аллокация вершин под спавн вместо пересборки рендерера; (б) реестр SWEP из Lua и вьюмодель; (в) вращение пропа колесом и заморозка у физгана; (г) скрытие своей модели, когда камера внутри неё; (д) скелет+анимации моделей; (е) консольные команды спавна на данных из 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 `. ## 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` + картовые тесты.