Files
fparkan/docs/specs/nres.md
Valentin Popov 31d849ddbf
Some checks failed
Test / Lint (push) Failing after 1m57s
Test / Test (push) Has been skipped
Test / Render parity (push) Has been skipped
updated docs
2026-02-19 16:10:57 +04:00

203 lines
9.1 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# NRes
`NRes` — базовый контейнер ресурсов движка Parkan: Iron Strategy.
Страница фиксирует формат на диске и runtime-контракт чтения/поиска/сохранения в высокоуровневом виде, без привязки к внутренним адресам и именам из дизассемблера.
Связанная страница:
- [RsLi](rsli.md)
## 1. Назначение
`NRes` используется как универсальный архив:
- 3D-модели (`*.msh`, `*.rlb`);
- текстуры (`Texm`);
- материалы (`MAT0`);
- эффекты (`FXID`);
- миссионные и служебные ресурсы.
Формат поддерживает:
- чтение;
- поиск по имени;
- редактирование (add/replace/remove);
- полную пересборку архива.
## 2. Общий layout файла
```text
[Header: 16]
[Data region: variable, 8-byte aligned chunks]
[Directory: entry_count * 64, всегда в конце файла]
```
Критично: каталог всегда расположен в конце файла.
## 3. Заголовок (16 байт)
Все значения little-endian.
| Offset | Size | Type | Значение |
|---:|---:|---|---|
| 0 | 4 | char[4] | `NRes` |
| 4 | 4 | u32 | `0x00000100` (версия 1.0) |
| 8 | 4 | i32 | `entry_count` (должен быть `>= 0`) |
| 12 | 4 | u32 | `total_size` (должен быть равен фактическому размеру файла) |
Производные значения:
- `directory_size = entry_count * 64`;
- `directory_offset = total_size - directory_size`.
Ограничения:
- `directory_offset >= 16`;
- `directory_offset + directory_size == total_size`.
## 4. Запись каталога (64 байта)
| Offset | Size | Type | Поле |
|---:|---:|---|---|
| 0 | 4 | u32 | `type_id` |
| 4 | 4 | u32 | `attr1` |
| 8 | 4 | u32 | `attr2` |
| 12 | 4 | u32 | `size` (размер payload) |
| 16 | 4 | u32 | `attr3` |
| 20 | 36 | char[36] | `name_raw` (C-строка) |
| 56 | 4 | u32 | `data_offset` |
| 60 | 4 | u32 | `sort_index` |
### 4.1. Имя ресурса (`name_raw`)
Контракт:
- максимум 35 полезных байт + NUL;
- допускается ровно один терминатор внутри 36-байтового поля;
- имя сравнивается регистронезависимо по ASCII-правилу (`A..Z` -> `a..z`).
Для writer/editor:
- запрещено писать NUL внутри полезной части имени;
- запрещены имена длиной > 35 байт.
### 4.2. Диапазон данных (`data_offset`, `size`)
Для каждой записи:
- `data_offset >= 16`;
- `data_offset + size <= directory_offset`.
Практически (канонический writer): каждый payload начинается с 8-байтного выравнивания.
## 5. Таблица сортировки (`sort_index`)
`sort_index` задает перестановку «отсортированный список -> исходный индекс записи».
Пусть:
- `entries[i]` — i-я запись каталога в исходном порядке;
- `P` — массив индексов `0..entry_count-1`, отсортированный по `entries[idx].name` (ASCII case-insensitive).
Тогда в канонической записи:
- `entries[i].sort_index = P[i]`.
Это именно таблица для бинарного поиска по имени, а не «ранг текущей записи».
## 6. Поиск по имени
Алгоритм поиска:
1. Выполнить бинарный поиск по диапазону `i in [0, entry_count)`.
2. На шаге `i` взять `target = entries[i].sort_index`.
3. Сравнить искомое имя с `entries[target].name` (ASCII case-insensitive).
4. При совпадении вернуть `target`.
Fail-safe поведение:
- если `sort_index` некорректен (выход за диапазон), реализация должна перейти на линейный fallback по всем записям;
- fallback использует то же ASCII case-insensitive сравнение.
## 7. Каноническая пересборка архива
Канонический writer выполняет:
1. Пишет заглушку заголовка (16 байт).
2. Пишет payload всех записей в текущем порядке.
3. После каждого payload добавляет 0-padding до кратности 8.
4. Пересчитывает `sort_index` через сортировку имен.
5. Дописывает каталог (`entry_count * 64`).
6. Пересчитывает и записывает `total_size`.
Итоговый файл должен удовлетворять всем ограничениям из разделов 35.
## 8. Режим `raw` (совместимость инструментов)
Для служебных инструментов допускается `raw_mode`:
- любой бинарный файл трактуется как один «сырой» ресурс;
- возвращается одна запись (`name = RAW`, `data_offset = 0`, `size = len(file)`).
Этот режим не является форматом `NRes` на диске, это только режим открытия.
## 9. Контрольные инварианты
Минимальный набор проверок при чтении:
1. `magic == "NRes"`.
2. `version == 0x100`.
3. `entry_count >= 0`.
4. `header.total_size == file_size`.
5. Каталог находится в конце файла.
6. Для каждой записи диапазон данных не пересекает каталог.
7. Имя корректно C-терминировано и не длиннее 35 байт.
Минимальный набор проверок при записи:
1. Все имена <= 35 байт и без внутренних NUL.
2. `sort_index` формирует валидную перестановку `0..N-1`.
3. Все паддинги между payload состоят из нулевых байт.
4. `total_size` равен фактической длине выходного файла.
## 10. Эмпирическая проверка на retail-корпусе
Валидация на полном наборе `testdata/Parkan - Iron Strategy`:
- найдено `120` архивов `NRes`;
- roundtrip `unpack -> repack -> byte-compare`: `120/120` совпали побайтно;
- критических расхождений формата не обнаружено.
Инструмент:
- `tools/archive_roundtrip_validator.py`
## 11. Статус покрытия и что осталось до 100%
Закрыто:
- формат заголовка/каталога;
- правила поиска;
- каноническая пересборка;
- строгие инварианты валидатора;
- побайтовый roundtrip на retail-корпусе.
Осталось до полного 100% архитектурного покрытия движка:
1. Формальная семантика `attr1/attr2/attr3` для всех типов ресурсов (частично вынесена в профильные страницы `msh`, `material`, `texture`, `fxid`, `terrain`).
2. Полная спецификация поведения при не-ASCII именах (в реальных игровых архивах используется ASCII-практика; для Unicode-коллации движок не документирован).
3. Полная спецификация платформенных гарантий атомарной записи (формат данных закрыт, но OS-уровневые гарантии замены файла зависят от платформы и файловой системы).
## 12. Специализация `objects.rlb`
Хотя `objects.rlb` формально является обычным `NRes`, его payload имеет отдельный семантический контракт:
- запись каталога соответствует одному объектному прототипу;
- payload записи - массив фиксированных ссылок `ObjectRef64` (`archive_name[32] + resource_name[32]`);
- runtime-резолв меша выполняется через эти ссылки, а не через имя entry `*.msh` внутри `objects.rlb`.
Это означает, что `objects.rlb` должен рассматриваться не как архив мешей, а как реестр привязок между mission/unit-ключами и фактическими ресурсами.
См. детальную страницу:
- [Object registry (`objects.rlb`)](object-registry.md)