From 0d7ae6a017b8b2bf26c5c14c39cb62b599e8262d Mon Sep 17 00:00:00 2001 From: Valentin Popov Date: Thu, 19 Feb 2026 11:07:04 +0400 Subject: Документирование и обновление спецификаций MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Обновлены спецификации `runtime-pipeline`, `sound`, `terrain-map-loading`, `texture`, `ui` и `wear`. - Добавлены разделы о статусе покрытия и оставшихся задачах для достижения 100% завершенности. - Внесены уточнения по архитектурным ролям, минимальным контрактам и требованиям к toolchain для каждой подсистемы. - Уточнены форматы данных и правила взаимодействия между компонентами системы. --- docs/specs/msh-notes.md | 321 ++++++++++++------------------------------------ 1 file changed, 81 insertions(+), 240 deletions(-) (limited to 'docs/specs/msh-notes.md') diff --git a/docs/specs/msh-notes.md b/docs/specs/msh-notes.md index 1bd4808..6e77c4f 100644 --- a/docs/specs/msh-notes.md +++ b/docs/specs/msh-notes.md @@ -1,277 +1,118 @@ # 3D implementation notes -Контрольные заметки, сводки алгоритмов и остаточные семантические вопросы по 3D-подсистемам. +Контрольная страница с практическими правилами реализации 3D-пайплайна и с перечнем незакрытых зон. +Документ intentionally high-level: без ссылок на внутренние функции/адреса. ---- +Связанные страницы: -## 5.1. Порядок байт +- [MSH core](msh-core.md) +- [MSH animation](msh-animation.md) +- [Material (`MAT0`)](material.md) +- [Texture (`Texm`)](texture.md) +- [FXID](fxid.md) +- [Render pipeline](render.md) -Все значения хранятся в **little‑endian** порядке (платформа x86/Win32). +## 1. Базовые двоичные правила -## 5.2. Выравнивание +1. Все форматы в этой подсистеме little-endian. +2. Внутри NRes данные ресурсов выравниваются по 8 байт. +3. Внутри payload таблиц padding между записями обычно отсутствует: записи идут подряд по stride. -- **NRes‑ресурсы:** данные каждого ресурса внутри NRes‑архива выровнены по границе **8 байт** (0‑padding). -- **Внутренняя структура ресурсов:** таблицы Res1/Res2/Res7/Res13 не имеют межзаписевого выравнивания — записи идут подряд. -- **Vertex streams:** stride'ы фиксированы (12/4/8 байт) — вершинные данные идут подряд без паддинга. +## 2. Быстрая карта stride'ов -## 5.3. Размеры записей на диске +| Ресурс | Запись | Stride | +|---|---|---:| +| Res1 | Node | 38 | +| Res2 | Slot | 68 (после header `0x8C`) | +| Res3 | Position | 12 | +| Res4 | Normal | 4 | +| Res5 | UV0 | 4 | +| Res6 | Index | 2 | +| Res7 | Tri descriptor | 16 | +| Res8 | Animation key | 24 | +| Res13 | Batch | 20 | +| Res19 | Animation map | 2 | -| Ресурс | Запись | Размер (байт) | Stride | -|--------|-----------|---------------|-------------------------| -| Res1 | Node | 38 | 38 (19×u16) | -| Res2 | Slot | 68 | 68 | -| Res3 | Position | 12 | 12 (3×f32) | -| Res4 | Normal | 4 | 4 (4×s8) | -| Res5 | UV0 | 4 | 4 (2×s16) | -| Res6 | Index | 2 | 2 (u16) | -| Res7 | TriDesc | 16 | 16 | -| Res8 | AnimKey | 24 | 24 | -| Res10 | StringRec | переменный | `4 + (len ? len+1 : 0)` | -| Res13 | Batch | 20 | 20 | -| Res19 | AnimMap | 2 | 2 (u16) | -| Res15 | VtxStr | 8 | 8 | -| Res16 | VtxStr | 8 | 8 (2×4) | -| Res18 | VtxStr | 4 | 4 | +## 3. Декодирование ключевых потоков -## 5.4. Вычисление количества элементов +## 3.1. Позиции (Res3) -Количество записей вычисляется из размера ресурса: +`float3`, stride `12`. -``` -count = resource_data_size / record_stride -``` - -Например: - -- `vertex_count = res3_size / 12` -- `index_count = res6_size / 2` -- `batch_count = res13_size / 20` -- `slot_count = (res2_size - 140) / 68` -- `node_count = res1_size / 38` -- `tri_desc_count = res7_size / 16` -- `anim_key_count = res8_size / 24` -- `anim_map_count = res19_size / 2` - -Для Res10 нет фиксированного stride: нужно последовательно проходить записи `u32 len` + `(len ? len+1 : 0)` байт. - -## 5.5. Идентификация ресурсов в NRes - -Ресурсы модели идентифицируются по полю `type` (смещение 0) в каталожной записи NRes. Загрузчик использует `niFindRes(archive, type, subtype)` для поиска, где `type` — число (1, 2, 3, ... 20), а `subtype` (byte) — уточнение (из аргумента загрузчика). - -## 5.6. Минимальный набор для рендера - -Для статической модели без анимации достаточно: - -| Ресурс | Обязательность | -|--------|------------------------------------------------| -| Res1 | Да | -| Res2 | Да | -| Res3 | Да | -| Res4 | Рекомендуется | -| Res5 | Рекомендуется | -| Res6 | Да | -| Res7 | Для коллизии | -| Res13 | Да | -| Res10 | Желательно (узловые имена/поведенческие ветки) | -| Res8 | Нет (анимация) | -| Res19 | Нет (анимация) | -| Res15 | Нет | -| Res16 | Нет | -| Res18 | Нет | -| Res20 | Нет | - -## 5.7. Сводка алгоритмов декодирования - -### Позиции (Res3) - -```python -def decode_position(data, vertex_index): - offset = vertex_index * 12 - x = struct.unpack_from(' list[str | None]: - out = [] - off = 0 - for _ in range(node_count): - ln = struct.unpack_from('> 8) & 1 - size = FX_CMD_SIZE[op] - cmds.append((op, enabled, ptr, size)) - ptr += size - if ptr != len(raw): - raise ValueError('tail bytes after command stream') - return cmds +```text +q = s16 / 32767.0 ``` -### Texm (header + mips + Page) - -```python -def parse_texm(raw: bytes): - magic, w, h, mips, f4, f5, unk6, fmt = struct.unpack_from('<8I', raw, 0) - assert magic == 0x6D786554 # 'Texm' - bpp = 1 if fmt == 0 else (2 if fmt in (565, 556, 4444) else 4) - pix_sum = 0 - mw, mh = w, h - for _ in range(mips): - pix_sum += mw * mh - mw = max(1, mw >> 1) - mh = max(1, mh >> 1) - off = 32 + (1024 if fmt == 0 else 0) + bpp * pix_sum - page = None - if off + 8 <= len(raw) and raw[off:off+4] == b'Page': - n = struct.unpack_from(' batch/triangles; + - batch -> indices; + - indices -> vertices; + - anim_map -> anim_keys. +4. Неизвестные поля и неизвестные ресурсы сохранять через copy-through. -Бинарный layout и декодер известны, но значения этих трёх полей в контенте используются контекстно; для 1:1 достаточно хранить/восстанавливать их без модификации. +## 5. Практический writer-контракт -## 6.6. Что пока не хватает для полноценного обратного экспорта (`OBJ -> MSH/NRes`) +1. Пересчитывать только явно вычислимые поля. +2. Не нормализовать opaque-данные без уверенной спецификации. +3. При roundtrip неизмененных данных требовать byte-identical результат. +4. Для новых ассетов фиксировать отдельную политику «генерация vs preserve». -Ниже перечислено то, что нужно закрыть для **lossless round-trip** и 1:1‑поведения при импорте внешней геометрии обратно в формат игры. +## 6. Runtime-связка материалов и текстур -### A) Неполная «авторская» семантика бинарных таблиц +Канонический путь резолва: -1. `Res2` header (`первые 0x8C`): не зафиксированы все поля и правила их вычисления при генерации нового файла (а не copy-through из оригинала). -2. `Res7` tri-descriptor: для 16‑байтной записи декодирован базовый каркас, но остаётся неформализованной часть служебных бит/полей, нужных для стабильной генерации adjacency/служебной топологии. -3. `Res13` поля `unk4/unk6/unk14`: для парсинга достаточно, но для генерации «канонических» значений из голого `OBJ` правила не определены. -4. `Res2` slot tail (`unk30..unk40`): семантика не разложена, поэтому при экспорте новых ассетов нет детерминированной формулы заполнения. +1. Модель -> wear-таблица (`*.wea`). +2. Wear-слот -> material name. +3. Material -> текущая фаза -> `textureName`. +4. `Texm` ищется в `Textures.lib` (или lightmap-библиотеке для lightmap-ветки). -### B) Анимационный path ещё не закрыт как writer +Fallback: -1. Нужен полный writer для `Res8/Res19`: - - точная спецификация байтового формата на запись; - - правила генерации mapping (`Res19`) по узлам/кадрам; - - жёсткая фиксация округления как в x87 path (включая edge-case на границах кадра). -2. Правила биндинга узлов/строк (`Res10`) и `slotFlags` к runtime‑сущностям пока описаны частично и требуют формализации именно для импорта новых данных. +- материал: `DEFAULT`, затем индекс `0`; +- текстура/lightmap: fallback-слот движка. -### C) Материалы, текстуры, эффекты для «полного ассета» +## 7. Что уже закрыто для 1:1 -1. Для `Texm` не завершён writer, покрывающий все используемые режимы (включая palette path, mip-chain, `Page`, и правила заполнения служебных полей). -2. Для `FXID` известен контейнер/длины команд, но не завершена field-level семантика payload всех opcode для генерации новых эффектов, эквивалентных оригинальному пайплайну. -3. Экспорт только `OBJ` покрывает геометрию; для игрового ассета нужен sidecar-слой (материалы/текстуры/эффекты/анимация), иначе импорт неизбежно неполный. +1. Бинарный контракт базовых MSH таблиц. +2. Контракт animation sampling (`Res8 + Res19`). +3. Контракт MAT0/WEAR/Texm на уровне чтения и применения в кадре. +4. Формат FXID-контейнера, командный поток и fixed command sizes. +5. Валидация на retail-корпусе через `tools/msh_doc_validator.py` (0 ошибок/предупреждений). -### D) Что это означает на практике +## 8. Статус покрытия и что осталось до 100% -1. `OBJ -> MSH` сейчас реалистичен как **ограниченный static-экспорт** (позиции/индексы/часть batch/slot структуры). -2. `OBJ -> полноценный игровой ресурс` (без потерь, с поведением 1:1) пока недостижим без закрытия пунктов A/B/C. -3. До закрытия пунктов A/B/C рекомендуется использовать режим: - - геометрия экспортируется из `OBJ`; - - неизвестные/служебные поля берутся copy-through из референсного оригинального ассета той же структуры. +1. Полная field-level семантика части служебных полей: + - `Batch20` opaque-поля; + - хвостовые служебные поля slot-записей; + - часть флагов узлов/групп. +2. Полный writer-путь для авторинга новых анимированных ассетов (не только roundtrip существующих). +3. Полная формализация семантики FX payload полей по каждому opcode для генерации новых эффектов, а не только для корректного чтения/исполнения. +4. Полный канонический writer `Texm` для всех редких форматов и edge-case комбинаций служебных флагов. +5. Сквозной «импорт внешнего ассета -> игровой пакет» с формальной спецификацией sidecar-метаданных (материал/эффект/анимация). -- cgit v1.2.3