From c9264fe8922fed2c3295f6b1eb7b3ab7a0207c8c Mon Sep 17 00:00:00 2001 From: Niko Marmeladkov Date: Mon, 12 Oct 2026 16:45:34 +0300 Subject: [PATCH] =?UTF-8?q?docs:=20=D0=BF=D0=BB=D0=B0=D0=BD=20=D1=81=D0=BE?= =?UTF-8?q?=D0=B2=D0=BC=D0=B5=D1=81=D1=82=D0=B8=D0=BC=D0=BE=D1=81=D1=82?= =?UTF-8?q?=D0=B8=20=D1=81=20GMod=20(=D0=BE=D0=B4=D0=B8=D0=BD=D0=BE=D1=87?= =?UTF-8?q?=D0=BD=D0=B0=D1=8F=20=D0=B8=D0=B3=D1=80=D0=B0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Зафиксирован план достижения одиночной игры, совместимой с GMod. Главный принцип — совместимость, а не тождество внутренностей: контент карт и аддонов, Lua-API, поведение игрока, формат сущностей. Часть A — семь найденных дефектов, в том числе подтверждённых замером по реальному gm_construct.bsp: 77 из 2203 брашей ошибочно считаются твёрдыми, скорость не гасится о контакт, нет отсечения граней, нет деталь-текстур. --- docs/PARITY.md | 343 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 343 insertions(+) create mode 100644 docs/PARITY.md diff --git a/docs/PARITY.md b/docs/PARITY.md new file mode 100644 index 0000000..39a5edc --- /dev/null +++ b/docs/PARITY.md @@ -0,0 +1,343 @@ +# План Source RS: одиночная игра, совместимая с GMod + +**Статус:** план согласован, начало исполнения (2026-10-12). +**Каталог:** `~/garrysmod-rs`. +**Связано:** `PLAN.md` (общий план), `PROBLEMS.md` (боли), `RECON.md` (реверс). + +Каждый этап — рабочий коммит и проверяемый результат, а не заготовка. + +--- + +## Главный принцип: совместимость, а не тождество внутренностей + +Решение пользователя: «1 в 1 не надо прям, но надо совместимость». + +Это меняет критерий готовности. Нас интересует не внутреннее устройство GMod, +а то, что снаружи него видно и от чего зависит чужой код: + +| Что должно работать | Почему это «совместимость» | +|---|---| +| **Контент** карт и аддонов: BSP, VTF, VMT, MDL, VTX, а также `lua/` с аддона | аддон физически не запустится без этого | +| **Lua-API**: `Entity()`, `ents.*`, `Vector`/`Angle`/`Color`, хуки, таймеры, `file`, `surface`, `sound`, `render`, `cam`, `Curves` | код аддона должен выполняться без правок | +| **Поведение игрока и физика** | геймплей аддона завязан на это | +| **Формат карт**: сущности читаем так же, как в Source | иначе нельзя открыть чужую карту | +| **Сетевой протокол** (уже сделано) | мультиплеер | + +Что **не** требуется: внутренние структуры движка, точное совпадение +побайтовых раскладок в нестандартных случаях, поддержка всего, что есть в +`base_ents` GMod. + +Практическое следствие: там, где можно, сверяемся с исходниками Source.NET и +реальными файлами, а не угадываем; а публичные Lua-имена и сигнатуры берём из +настоящих `lua/includes/` GMod, а не придумываем. + +--- + +## Порядок (подтверждён) + +**A → B → C → D → E**, причём **свет (C) раньше game.dll (E2)** — так решил +пользователь. + +--- + +## Часть A. Найденные дефекты (то, что сломано прямо сейчас) + +Часть пунктов пользователь заметил визуально, часть подтверждена замером по +реальному `gm_construct.bsp`. + +### A1. Коллизия: скорость не гасится о поверхность +**Симптом** (пользователь): «подхожу в стену, отпускаю клавиши — входит внутрь +чуть-чуть, потом выталкивает». + +**Причина** (`crates/engine/src/player.rs:432`): `move_aabb()` возвращает только +`(Aabb, on_ground)` и **не трогает `self.velocity`**. Каждый кадр игрок +сдвигается на `velocity * dt` внутрь стены, получает выталкивание и оказывается +на грани — визуально «ныряет» и отскакивает. Горизонтальная скорость в стену +никогда не обнуляется. + +**Как в Source**: `CMoveData` отдаёт `clipNormal`, скорость проектируется на +плоскость контакта: `v -= n * min(0, dot(v, n))`. + +**Исправление**: `move_aabb` возвращает нормали контакта; `Player::update` гасит +компоненту скорости внутрь каждой нормали и обнуляет `velocity.z` при +`on_ground`. Плюс явное торможение при отпускании клавиш. + +**Приёмка**: тест «в стену на полной скорости → после отпускания стоит +неподвижно, `|velocity.xz| < 1`, положение не дрейфует»; тест на отсутствие +просадки в стену при `dt = 1/60` и скорости 250. + +### A2. Коллизия: твёрдость браша определяется неверно +**Симптомы**: «невозможно пройти в здания, хотя проход есть», «вода твёрдая». + +**Причина** (`player.rs:43`): `const TRIGGER: i32 = 0x4000_0000;` и проверка +`if contents == TRIGGER { continue }`. То есть твёрдым считается **всё, кроме +ровно `0x40000000`** — включая браши с `contents == 0` (по спецификации Source +`CONTENTS_SOLID = 0x1`, всё остальное не твёрдо) и браши воды/тумана. + +**Замер по `gm_construct.bsp` (2203 браша, лумп 18):** + +| contents | смысл | брашей | сейчас твёрдые | по спецификации | +|---|---|---|---|---| +| `0x08000001` | SOLID | 1130 | да | да | +| `0x00000001` | SOLID | 996 | да | да | +| `0x00000000` | EMPTY (не твёрдый) | **70** | **да** ✗ | нет | +| `0x08000080` | не SOLID | 3 | **да** ✗ | нет | +| `0x18000008` | не SOLID | 2 | **да** ✗ | нет | +| `0x10000020` | **WATER** | 1 | **да** ✗ | нет | +| `0x30000008` | не SOLID | 1 | **да** ✗ | нет | + +Итого **77 из 2203 брашей (3.5%) считаются твёрдыми ошибочно** — невидимые стены +в проходах и твёрдая вода озера. + +**Исправление**: твёрдым считать браш, у которого установлен бит +`CONTENTS_SOLID (0x1)`. Отдельно разобрать воду (`0x20`), туман (`0x40`), +окна (`0x2`) и решётки (`0x8`): они не останавливают игрока, но влияют на +жидкость и свет. + +**Приёмка**: счётчик брашей по `contents` в логе; на gm_construct ожидается +2126 твёрдых и 77 не твёрдых; тесты «проход в здание открыт», «в воду можно +войти». + +### A3. Коллизия: нет broadphase для брашей +2203 браша проверяются целиком на каждом подшаге, в каждой из 4 итераций +разрешения. Для displacement'ов есть сетка, для брашей — нет. + +**Исправление**: 2D-сетка по XY с AABB каждого браша; запрос кандидатов по +расширенному AABB хода. + +**Приёмка**: замер времени кадра коллизии до/после; цель — стабильные 60 fps +при обычном движении. + +### A4. Рендер: отсечения граней нет вообще +**Симптом**: «видно обратную сторону брашей». + +**Причина** (`crates/render/src/world_renderer.rs`): `cull_mode: None` во всех +**шести** пайплайнах (строки 247, 292, 385, 483, 613). + +**Уточнение пользователя**: в оригинале сквозь стены тоже видно — но там это +намеренно (вид сквозь стену на HUD), а не отсутствие backface culling. + +**Исправление**: `CullMode::Back` для мира и моделей; для двусторонней +геометрии — отдельный проход `CullMode::Front` с инверсией нормали (как в +Source: сначала фронт с инверсией, затем бэк). + +**Приёмка**: в галерее gm_construct не видно изнанки стен. + +### A5. Рендер: деталь-текстуры не поддержаны → нет песка у озера +**Симптом**: «у озера трава должна переходить в песок, песка нет». + +**Причина**: в `crates/formats/src/bsp.rs` перечислены 64 лумпа, и **деталь-текстур +среди них нет**. В Source 1 они лежат в `GAME_LUMP` как под-лумп +`detailtextures.dll` (`LUMP_DETAILTEXTURES = 12`) и накладываются поверх базовой +текстуры. Шейдер `world.wgsl` (38 строк) читает только `base_texture` и +умножает на вершинный `light`. + +**Исправление**: +1. Разбор `GAME_LUMP` (запись лумпа **16 байт**: `fileofs, disksize, offset, + fourCC`), под-лумп 12 → список `(текстура, scaleX, scaleY)`. +2. Проекция деталь-текстуры на мировые координаты по UV грани. +3. Шейдер: дополнительные сэмплы со смешением, `$detail` из VMT. + +**Приёмка**: у озера gm_construct виден переход травы в песок. + +### A6. Белая нетекстурированная поверхность (было: «белые панели на 0 0 0») +**Симптом по скриншоту**: большая белая polygonal-поверхность, лежащая на +земле рядом с озером (камера на `pos -1070 1565 533`). Контур с прямыми +отрезками и острыми углами, поверхность явно повторяет рельеф местности. +Окружающие дома, трава, дороги и вода (слева, тёмно-синяя) выглядят нормально. + +**Это не пропы.** Проп — это объёмная модель с чёткими гранями; здесь же +плоская поверхность, граница которой идёт по рельефу. Значит это браш- или +displacement-геометрия, у которой **не разрешилась текстура** — материал +рисуется `fallback`-белым (`Material::fallback`, `world.rs:739`). + +Ранее я проверил пропы и ничего подозрительного не нашёл: пропов без `origin` +нет, с `origin = "0 0 0"` нет, наш код порождает всего 9 моделей +(5 × `prop_dynamic lights_flescent01a`, 4 × `prop_physics` мебели) — все с +корректными координатами. Это гипотеза отвергнута, версия снимка была неверной. + +**Кандидаты причины, по убыванию вероятности**: +1. **Не найдена VTF-текстура** для части поверхностей — `fallback` рисует белым. + Особенно вероятно для displacement-поверхностей у воды: у них отдельные + `texinfo`, которые не всегда есть в списке. +2. **Не разрешён VMT** — тогда материал падает в тот же белый fallback. +3. Отдельная поверхность, которой полагается деталь-текстура, но базовая + не загрузилась (связано с A5). + +**Действие** (одна итерация, даёт точный ответ): дамп всех материалов мира — +`{ имя VMT, имя VTF, загрузилась ли, причина отказа }` плюс AABB каждого +батча. По скриншоту видно, что проблема локальная (один район карты), значит +список «отвалившихся» текстур будет небольшим и читаемым. + +**Побочный вывод**: белый fallback — плохая диагностика и плохое поведение. +Нужна проверка в `doctor` (см. B4) и осмысленная заглушка: тёмно-шахматная +вместо белой, чтобы выделять проблему, а не маскировать её. + +### A8. Знаки и порядок осей при повороте моделей +`rotate_model_vector` строит базис `forward/left/up`, тогда как Source +использует `forward/right/up`. Модельная система координат MDL — правая +(X вперёд, Y влево, Z вверх), поэтому знак оси Y критичен: ошибка +разворачивает модель зеркально и ломает нормали. + +Это не «на глаз» — сверяем с эталонным `AngleVectors` и закрываем unit-тестом +на известных углах (`0 0 0`, `0 90 0`, `0 0 90`, `90 0 0`), фиксируя +ожидаемые векторы из формулы Source. + +### A7. Вода выглядит неправильно +**Симптом**: «просто какое-то отражение неба, никакой текстуры, твёрдая». + +**Причины**: +- отражение — **зашитая константа** `vec3(0.64, 0.76, 0.90)` в + `water.wgsl:49`, а не окружение; +- одна поверхность с `discard` по знаку `facing`, тогда как в Source их две + (`$abovewater` 1 и 0) — верх и изнанка; +- нет преломления, поглощения по глубине, блика солнца, нормалей из текстуры, + `$fogcolor`-затухания; +- твёрдость — из A2. + +**Исправление**: две поверхности; отражение из окружения; преломление через +цвет буфера; поглощение по расстоянию; specular от солнца; плюс A2. + +--- + +## Часть B. Фундамент: система энтити + +Без неё не появляется ни свет, ни Lua над энтити, ни физика. Свет — это тоже +энтити (`light_environment`, `light`, `light_spot`). + +**B1. Ядро** (`crates/engine/src/entity.rs`) +- `EntityIndex(u16)` — слот + `generation` для безопасных «умерших» указателей; +- `Entity` — classname, keyvalues, transform (`origin`/`angles`/local-origin), + флаги (`solid`, `movetype`, `model`, `render`/`rendergroup`); +- `EntityWorld` — пул, `spawn(kv)`, `remove`, итерация по classname, + отложенное удаление (как в Source, чтобы не инвалидировать ссылки в кадре); +- лимиты из файлов: максимум энтити, максимум ключей, максимум длины значения + (правило #1 AGENTS.md — никаких `unwrap` на данных из файлов). + +**B2. Спавн из карты**: `build_world` перестаёт вручную порождать модели — он +скармливает лумп `ENTITIES` в `EntityWorld`, а рендер и коллизия читают из неё. +Это убирает белые панели как класс проблем и даёт всем остальным этапам единый +источник истины. + +**B3. Коллизия по энтити**: браши `func_*`/`trigger_*`/`info_null` с корректной +твёрдостью; AABB/OBB для моделей; `trigger`-зоны отдельным слоем. + +**B4. Отладка**: `ent_list` / `ent_info ` — иначе каждый следующий +дефект снова придётся ловить на глаз. + +**Приёмка**: `ent_list` показывает все энтити gm_construct с корректными +координатами; коллизия `func_*` работает. + +--- + +## Часть C. Освещение (включая солнце) + +Сейчас в шейдере один попиксельно интерполированный `light: vec3` из вершин — +меньше, чем в Source даже на простых картах. + +**C1. Свет из карты**: `light`, `light_spot`, `light_dynamic` (в gm_construct: +108 `light` + 28 `light_spot`), `light_environment` — это и есть солнце +(направление, цвет, интенсивность, флаг «небо»). + +**C2. Модель освещения в шейдере**: динамический свет (солнце + до N источников +в радиусе) поверх статического; источники ограничить по экрану и расстоянию. + +**C3. Sun-тени**: каскадные shadow maps (в Source 3–4 каскада). Начать с одного +каскада 2048². + +**C4. Lightmap**: лумп `LIGHTING`/`LIGHTING_HDR` (12 МБ в gm_construct) — +попиксельно вместо вершинного; даёт качественный свет в помещениях. + +**C5. Материал**: `Phong`-подобная модель — diffuse + specular + fresnel; +`$phong`/`$specular`/`$color2` из VMT; самозатенение от карты нормалей. + +**C6. Небо и `env_*`**: `env_sky`, `env_fog`, `env_sun`, `env_tonemap`, +`env_cubemap`, `env_skybox`. + +**Приёмка**: на gm_construct видны солнечные тени от построек, помещения +освещены не плоско, вода отражает окружение, есть спекуляр на металле. + +--- + +## Часть D. Lua поверх энтити + +Текущий `lua` (1278 строк) — `print`, `hook.*`, `timer.Simple`, +`concommand.*`, заглушки. Ни типов, ни энтити. + +**D1. Типы**: `Vector`, `Angle`, `Color` как userdata с метаметодами GMod +(арифметика, `Angle:Forward()`, `Vector:Distance`, `:Angle`). + +**D2. `Entity(index)`** и методы: `GetPos/SetPos`, `GetAngles/SetAngles`, +`GetClass/GetModel/SetModel`, `Remove`, `IsValid`, `PhysicsInit*`. + +**D3. `ents.*`**: `Create`, `FindByClass`, `FindInSphere`, `GetAll`, `GetCount`. + +**D4. Сетевые поля**: `SetNW*`/`GetNW*` (в одиночке локальные, но с тем же API, +чтобы потом не переписывать аддоны). + +**D5. Библиотеки GMod**: `file.*` (с песочницей по AGENTS.md), `surface.*`, +`sound.*`, `render.*`, `cam.*`, `Curves`, `system.*`, `scrconcmd`, +`LocalPlayer`. `http` — только после песочницы. + +**Приёмка**: скрипт из гмода выполняется; список функций совместимости +(набранный из настоящих `lua/includes/modules/`) проходит. + +--- + +## Часть E. Одиночная игра как в GMod + +**E1. Player**: ходьба по ступеням (step-up ~18 units), лестницы, приседание с +плавным переходом, плавание (`Start Swim`), ноклип, падение с уроном. + +**E2. game.dll на Lua**: загрузка `gamemodes/sandbox/gamemode/init.lua` — +списки `ents`, `weapons`, `NPCs`, хуки `Initialize`, `Think`, `PlayerSpawn`. +Это то, что делает GMod «GMod’ом», а не просто картой Source. + +**E3. Оружие и предметы**: `weapon_*`, `item_*` через Lua. + +**E4. Физика пропов**: Jolt — `prop_physics` уже приходят с карты, но не падают +и не сталкиваются. + +**E5. NPC**: начиная с базовых `npc_*`. + +--- + +## Порядок и разумная дистанция до GMod + +GMod — это ~1 млн строк C++ плюс 15 лет аддон-экосистемы. Полное «1 в 1» +недостижимо как финальное состояние, но достижимо как наращиваемое покрытие: +A → B → C → D → E даёт к каждому моменту работающий одиночный клиент. + +| Этап | Что даёт пользователю | +|---|---| +| A | «стены не пускают, вода не твёрдая, белая поверхность устранена, песок появился» | +| B | карта описывается объектами — база для всего | +| C | солнце, тени, нормальный свет | +| D | Lua-аддоны запускаются на нашей карте | +| E | одиночная игра, совместимая с аддонами | + +Честное ограничение: «все энтити» — сотни классов. Но `base_ents` GMod — это +**Lua-скрипты**, а не C++. Когда появится исполнение `lua/entities/` и +`lua/weapons/`, значительная часть этих классов заработает сама, из исходников +GMod. Поэтому наш код на этих этапах делает только то, чего в Lua нельзя: +низкий уровень графики, физика, загрузка ресурсов. + +Свой набор «тяжёлых» классов держим минимальным: `prop_physics`, `func_door`, +`func_button`, `trigger_*`, `light_*`, `env_*`, `info_*`. Остальное — из Lua. + +--- + +## Решения, принятые пользователем + +1. **Скриншот получен** → A6 переформулирован: это не панели пропов, а + нетекстурированная браш/displacement-поверхность. Версия про «панели в 0 0 0» + отвергнута. +2. **`AngleVectors`** → вопрос снят, работа делается как A8: сверка с + эталонной формулой Source и фиксация unit-тестом, без опоры на память. +3. **Приоритет** → свет (C) раньше game.dll (E2). + +## Что осталось уточнить + +Ничего блокирующего. Дополнительно уточню по ходу: +- белый fallback заменить на тёмно-шахматный (A6) — согласую при реализации, + это меняет вид карты при недогруженных текстурах. \ No newline at end of file