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

9.1 KiB
Raw Blame History

NRes

NRes — базовый контейнер ресурсов движка Parkan: Iron Strategy.
Страница фиксирует формат на диске и runtime-контракт чтения/поиска/сохранения в высокоуровневом виде, без привязки к внутренним адресам и именам из дизассемблера.

Связанная страница:

1. Назначение

NRes используется как универсальный архив:

  • 3D-модели (*.msh, *.rlb);
  • текстуры (Texm);
  • материалы (MAT0);
  • эффекты (FXID);
  • миссионные и служебные ресурсы.

Формат поддерживает:

  • чтение;
  • поиск по имени;
  • редактирование (add/replace/remove);
  • полную пересборку архива.

2. Общий layout файла

[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-ключами и фактическими ресурсами.

См. детальную страницу: