- Текст 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 — зелёные.
81 KiB
Дневник агента: где я и что дальше
Живой документ. Обновляю по ходу работы, чтобы после автоматического уплотнения контекста ничего не терялось. Коротко: состояние, замеры, следующий шаг, грабли.
Обновлён: 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_substeps4,num_solver_iterations8. Тест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% ошибки. Гравитация на удержании выключена, тело сталкивается с миром. Отдельного демпфирования нет: скорость задаётся напрямую (в GModphysgun_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_ICONS512 → 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 закрывает всегда. В фокусеDTextEntryQ печатается: клиент проверяет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 /`. - Проверки:
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_CLOSESWEP-атака/переключение работают; 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=123,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):
- A23 оружие (SWEP из Lua + вьюмодель + аттачменты) — сюда же A20 (модель игрока: один и тот же рендер моделей).
- A21 физика взаимодействия: физган (луч → проп → таскание, вращение, заморозка), без полной гибернации.
- A24 спавн: сначала консольные команды на данных из Lua (
gm_spawn,gm_give, режимыgmod_tool), потом окно Q. - A22 Sandbox целиком: undo/cleanup/duplicator.
- Визуальная точность (A10 свет на пиксель, A11–A12 вода, A16 трава/декали, A18 скайбокс) — параллельно, Sandbox не блокирует.
2. Где я сейчас (в работе)
A20/A23, первый срез сделан: модель игрока в мире. Рендер тот же, что у
пропов (статический меш PropMesh), скелет и анимации — следующий срез.
Как включается и как проверять:
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 и ранний выход по совпадению позы —
вероятно, тело «спит» после телепорта). Проверять в интерактиве: заспавнить
арбуз, посмотреть, что он падает на экране.
Найдены и закрыты две поломки по жалобе «физика висит на экране»:
move_propостался со своей копией перезаписи вершин и не запоминал диапазоны — теперь он идёт черезrewrite_instance(замер: было «диапазонов 0» каждый шаг, стало «диапазонов 1 [(39585, 8)]»).- Буфер вершин мира создавался без
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-скиннинг (виден кадром).
Следующий шаг — проигрывание. План, чтобы не искать заново:
PropMesh(илиModelInstance) должен держать данные анимации:.ani(7 МБ) + список блоков + описания анимаций, ссылкойArc, — сейчас они читаются при загрузке модели и выбрасываются;- в движке
pose_model_instance(index, origin, angles, bone_quats: &[[f32;4]]): локальные кватернионы костей в мировые через родителей (world[i] = world[parent] · (T(pos_i) · R(q_i) · T(−pos_i))), затем скиннинг теми же весами (функция скиннинга уже есть — сейчас её ведёт синус); - в клиенте: выбрать секвенцию (
@idle_all_01через последовательность activity), взять кадр по времени (t · fps, циклом), собрать кватернионы костей из дорожек и позватьpose_model_instance; - затем выбор по состоянию (
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,d35efefA13 туман сделан целиком (чтение + шейдер);72bad60A19: подшаг не гасит скорость о склон (35°: 65 → 147);5bdbdbf,e638607A19: замер по углам + найден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_normal0,7 (45,6°),DIST_EPSILON1/32,sv_stepsize18, воздушный потолок желаемой скорости 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+ картовые тесты.