docs: план совместимости с GMod (одиночная игра)
Зафиксирован план достижения одиночной игры, совместимой с GMod. Главный принцип — совместимость, а не тождество внутренностей: контент карт и аддонов, Lua-API, поведение игрока, формат сущностей. Часть A — семь найденных дефектов, в том числе подтверждённых замером по реальному gm_construct.bsp: 77 из 2203 брашей ошибочно считаются твёрдыми, скорость не гасится о контакт, нет отсечения граней, нет деталь-текстур.
This commit is contained in:
parent
77d70f6b9f
commit
c9264fe892
1 changed files with 343 additions and 0 deletions
343
docs/PARITY.md
Normal file
343
docs/PARITY.md
Normal file
|
|
@ -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<f32>(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 <classname>` — иначе каждый следующий
|
||||
дефект снова придётся ловить на глаз.
|
||||
|
||||
**Приёмка**: `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) — согласую при реализации,
|
||||
это меняет вид карты при недогруженных текстурах.
|
||||
Loading…
Add table
Reference in a new issue