2026-02-19 11:07:04 +04:00
# NRes
2026-02-10 00:30:25 +04:00
2026-02-19 11:07:04 +04:00
`NRes` — базовый контейнер ресурсов движка Parkan: Iron Strategy.
Страница фиксирует формат на диске и runtime-контракт чтения/поиска/сохранения в высокоуровневом виде, без привязки к внутренним адресам и именам из дизассемблера.
2026-02-10 00:30:25 +04:00
2026-02-19 11:07:04 +04:00
Связанная страница:
2026-02-10 00:30:25 +04:00
2026-02-19 11:07:04 +04:00
- [RsLi ](rsli.md )
2026-02-10 00:30:25 +04:00
2026-02-19 11:07:04 +04:00
## 1. Назначение
2026-02-10 00:30:25 +04:00
2026-02-19 11:07:04 +04:00
`NRes` используется как универсальный архив:
2026-02-10 00:30:25 +04:00
2026-02-19 11:07:04 +04:00
- 3D-модели (`*.msh` , `*.rlb` );
- текстуры (`Texm` );
- материалы (`MAT0` );
- эффекты (`FXID` );
- миссионные и служебные ресурсы.
2026-02-10 00:30:25 +04:00
2026-02-19 11:07:04 +04:00
Формат поддерживает:
2026-02-10 00:30:25 +04:00
2026-02-19 11:07:04 +04:00
- чтение;
- поиск по имени;
- редактирование (add/replace/remove);
- полную пересборку архива.
2026-02-10 00:30:25 +04:00
2026-02-19 11:07:04 +04:00
## 2. Общий layout файла
2026-02-10 00:30:25 +04:00
2026-02-19 11:07:04 +04:00
```text
[Header: 16]
[Data region: variable, 8-byte aligned chunks]
[Directory: entry_count * 64, всегда в конце файла]
2026-02-10 00:30:25 +04:00
```
2026-02-19 11:07:04 +04:00
Критично: каталог всегда расположен в конце файла.
2026-02-10 00:30:25 +04:00
2026-02-19 11:07:04 +04:00
## 3. Заголовок (16 байт)
2026-02-10 00:30:25 +04:00
2026-02-19 11:07:04 +04:00
В с е значения little-endian.
2026-02-10 00:30:25 +04:00
2026-02-19 11:07:04 +04:00
| 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` (должен быть равен фактическому размеру файла) |
2026-02-10 00:30:25 +04:00
2026-02-19 11:07:04 +04:00
Производные значения:
2026-02-10 00:30:25 +04:00
2026-02-19 11:07:04 +04:00
- `directory_size = entry_count * 64` ;
- `directory_offset = total_size - directory_size` .
2026-02-10 00:30:25 +04:00
2026-02-19 11:07:04 +04:00
Ограничения:
2026-02-10 00:30:25 +04:00
2026-02-19 11:07:04 +04:00
- `directory_offset >= 16` ;
- `directory_offset + directory_size == total_size` .
2026-02-10 00:30:25 +04:00
2026-02-19 11:07:04 +04:00
## 4. Запись каталога (64 байта)
2026-02-10 00:30:25 +04:00
2026-02-19 11:07:04 +04:00
| 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` |
2026-02-10 00:30:25 +04:00
2026-02-19 11:07:04 +04:00
### 4.1. Имя р е с у р с а (`name_raw`)
2026-02-10 00:30:25 +04:00
2026-02-19 11:07:04 +04:00
Контракт:
2026-02-10 00:30:25 +04:00
2026-02-19 11:07:04 +04:00
- максимум 35 полезных байт + NUL;
- допускается ровно один терминатор внутри 36-байтового поля;
- имя сравнивается регистронезависимо по ASCII-правилу (`A..Z` -> `a..z` ).
2026-02-10 00:30:25 +04:00
2026-02-19 11:07:04 +04:00
Для writer/editor:
2026-02-10 00:30:25 +04:00
2026-02-19 11:07:04 +04:00
- запрещено писать NUL внутри полезной части имени;
- запрещены имена длиной > 35 байт.
2026-02-10 00:30:25 +04:00
2026-02-19 11:07:04 +04:00
### 4.2. Диапазон данных (`data_offset`, `size`)
2026-02-10 00:30:25 +04:00
2026-02-19 11:07:04 +04:00
Для каждой записи:
2026-02-10 00:30:25 +04:00
2026-02-19 11:07:04 +04:00
- `data_offset >= 16` ;
- `data_offset + size <= directory_offset` .
2026-02-10 00:30:25 +04:00
2026-02-19 11:07:04 +04:00
Практически (канонический writer): каждый payload начинается с 8-байтного выравнивания.
2026-02-10 00:30:25 +04:00
2026-02-19 11:07:04 +04:00
## 5. Таблица сортировки (`sort_index`)
2026-02-10 00:30:25 +04:00
2026-02-19 11:07:04 +04:00
`sort_index` задает перестановку «отсортированный список -> исходный индекс записи».
2026-02-10 00:30:25 +04:00
2026-02-19 11:07:04 +04:00
Пусть:
2026-02-10 00:30:25 +04:00
2026-02-19 11:07:04 +04:00
- `entries[i]` — i-я запись каталога в исходном порядке;
- `P` — массив индексов `0..entry_count-1` , отсортированный по `entries[idx].name` (ASCII case-insensitive).
2026-02-10 00:30:25 +04:00
2026-02-19 11:07:04 +04:00
Тогда в канонической записи:
2026-02-10 00:30:25 +04:00
2026-02-19 11:07:04 +04:00
- `entries[i].sort_index = P[i]` .
2026-02-10 00:30:25 +04:00
2026-02-19 11:07:04 +04:00
Это именно таблица для бинарного поиска по имени, а не «ранг текущей записи».
2026-02-10 00:30:25 +04:00
2026-02-19 11:07:04 +04:00
## 6. Поиск по имени
2026-02-10 00:30:25 +04:00
2026-02-19 11:07:04 +04:00
Алгоритм поиска:
2026-02-10 00:30:25 +04:00
2026-02-19 11:07:04 +04:00
1. Выполнить бинарный поиск по диапазону `i in [0, entry_count)` .
2. Н а шаге `i` взять `target = entries[i].sort_index` .
3. Сравнить искомое имя с `entries[target].name` (ASCII case-insensitive).
4. При совпадении вернуть `target` .
2026-02-10 00:30:25 +04:00
2026-02-19 11:07:04 +04:00
Fail-safe поведение:
2026-02-10 00:30:25 +04:00
2026-02-19 11:07:04 +04:00
- если `sort_index` некорректен (выход за диапазон), реализация должна перейти на линейный fallback по всем записям;
- fallback использует то же ASCII case-insensitive сравнение.
2026-02-10 00:30:25 +04:00
2026-02-19 11:07:04 +04:00
## 7. Каноническая пересборка архива
2026-02-10 00:30:25 +04:00
2026-02-19 11:07:04 +04:00
Канонический writer выполняет:
2026-02-10 00:30:25 +04:00
2026-02-19 11:07:04 +04:00
1. Пишет заглушку заголовка (16 байт).
2. Пишет payload всех записей в текущем порядке.
3. После каждого payload добавляет 0-padding до кратности 8.
4. Пересчитывает `sort_index` через сортировку имен.
5. Дописывает каталог (`entry_count * 64` ).
6. Пересчитывает и записывает `total_size` .
2026-02-10 00:30:25 +04:00
2026-02-19 11:07:04 +04:00
Итоговый файл должен удовлетворять всем ограничениям из разделов 3– 5.
2026-02-10 00:30:25 +04:00
2026-02-19 11:07:04 +04:00
## 8. Режим `raw` (совместимость инструментов)
2026-02-10 00:30:25 +04:00
2026-02-19 11:07:04 +04:00
Для служебных инструментов допускается `raw_mode` :
2026-02-10 00:30:25 +04:00
2026-02-19 11:07:04 +04:00
- любой бинарный файл трактуется как один «сырой» р е с у р с ;
- возвращается одна запись (`name = RAW` , `data_offset = 0` , `size = len(file)` ).
2026-02-10 00:30:25 +04:00
2026-02-19 11:07:04 +04:00
Этот режим не является форматом `NRes` на диске, это только режим открытия.
2026-02-10 00:30:25 +04:00
2026-02-19 11:07:04 +04:00
## 9. Контрольные инварианты
2026-02-10 00:30:25 +04:00
2026-02-19 11:07:04 +04:00
Минимальный набор проверок при чтении:
2026-02-10 00:30:25 +04:00
2026-02-19 11:07:04 +04:00
1. `magic == "NRes"` .
2. `version == 0x100` .
3. `entry_count >= 0` .
4. `header.total_size == file_size` .
5. Каталог находится в конце файла.
6. Для каждой записи диапазон данных не пересекает каталог.
7. Имя корректно C-терминировано и не длиннее 35 байт.
2026-02-10 00:30:25 +04:00
2026-02-19 11:07:04 +04:00
Минимальный набор проверок при записи:
2026-02-10 00:30:25 +04:00
2026-02-19 11:07:04 +04:00
1. В с е имена <= 35 байт и без внутренних NUL.
2. `sort_index` формирует валидную перестановку `0..N-1` .
3. В с е паддинги между payload состоят из нулевых байт.
4. `total_size` равен фактической длине выходного файла.
2026-02-10 00:30:25 +04:00
2026-02-19 11:07:04 +04:00
## 10. Эмпирическая проверка на retail-корпусе
2026-02-10 00:30:25 +04:00
2026-02-19 11:07:04 +04:00
Валидация на полном наборе `testdata/Parkan - Iron Strategy` :
2026-02-10 00:30:25 +04:00
2026-02-19 11:07:04 +04:00
- найдено `120` архивов `NRes` ;
- roundtrip `unpack -> repack -> byte-compare` : `120/120` совпали побайтно;
- критических расхождений формата не обнаружено.
2026-02-10 00:30:25 +04:00
2026-02-19 11:07:04 +04:00
Инструмент:
2026-02-10 00:30:25 +04:00
2026-02-19 11:07:04 +04:00
- `tools/archive_roundtrip_validator.py`
2026-02-10 00:30:25 +04:00
2026-02-19 11:07:04 +04:00
## 11. Статус покрытия и что осталось до 100%
2026-02-10 01:58:16 +04:00
2026-02-19 11:07:04 +04:00
Закрыто:
2026-02-10 01:58:16 +04:00
2026-02-19 11:07:04 +04:00
- формат заголовка/каталога;
- правила поиска;
- каноническая пересборка;
- строгие инварианты валидатора;
- побайтовый roundtrip на retail-корпусе.
2026-02-10 01:58:16 +04:00
2026-02-19 11:07:04 +04:00
Осталось до полного 100% архитектурного покрытия движка:
2026-02-10 01:58:16 +04:00
2026-02-19 11:07:04 +04:00
1. Формальная семантика `attr1/attr2/attr3` для всех типов ресурсов (частично вынесена в профильные страницы `msh` , `material` , `texture` , `fxid` , `terrain` ).
2. Полная спецификация поведения при не-ASCII именах (в реальных игровых архивах используется ASCII-практика; для Unicode-коллации движок не документирован).
3. Полная спецификация платформенных гарантий атомарной записи (формат данных закрыт, но OS-уровневые гарантии замены файла зависят от платформы и файловой системы).