diff options
Diffstat (limited to 'docs/specs/assets/nres/overview.md')
| -rw-r--r-- | docs/specs/assets/nres/overview.md | 608 |
1 files changed, 0 insertions, 608 deletions
diff --git a/docs/specs/assets/nres/overview.md b/docs/specs/assets/nres/overview.md deleted file mode 100644 index 60352cc..0000000 --- a/docs/specs/assets/nres/overview.md +++ /dev/null @@ -1,608 +0,0 @@ -# Документация по формату NRes - -## Обзор - -NRes — это формат контейнера ресурсов, используемый в игровом движке Nikita. Файл представляет собой архив, содержащий несколько упакованных файлов с метаданными и поддержкой различных методов сжатия. - -## Структура файла NRes - -### 1. Заголовок файла (16 байт) - -```c -struct NResHeader { - uint32_t signature; // +0x00: Сигнатура "NRes" (0x7365524E в little-endian) - uint32_t version; // +0x04: Версия формата (0x00000100 = версия 1.0) - uint32_t fileCount; // +0x08: Количество файлов в архиве - uint32_t fileSize; // +0x0C: Общий размер файла в байтах -}; -``` - -**Детали:** - -- `signature`: Константа `0x7365524E` (1936020046 в десятичном виде). Это ASCII строка "NRes" в обратном порядке байт -- `version`: Всегда должна быть `0x00000100` (256 в десятичном виде) для версии 1.0 -- `fileCount`: Общее количество файлов в архиве (используется для валидации) -- `fileSize`: Полный размер NRes файла, включая заголовок - -### 2. Данные файлов - -Сразу после заголовка (с offset 0x10) начинаются данные упакованных файлов. Они хранятся последовательно, один за другим. Точное расположение каждого файла определяется записью в каталоге (см. раздел 3). - -**⚠️ ВАЖНО: Выравнивание данных** - -Данные каждого файла **выравниваются по границе 8 байт**. После записи данных файла добавляется padding (нулевые байты) до ближайшего кратного 8 адреса. - -**Формула выравнивания:** - -``` -aligned_size = (packed_size + 7) & ~7 -padding_bytes = aligned_size - packed_size -``` - -**Пример:** - -- Файл размером 100 байт → padding 4 байта (до 104) -- Файл размером 104 байт → padding 0 байт (уже выровнен) -- Файл размером 105 байт → padding 3 байта (до 108) - -Это означает, что: - -1. `dataOffset` следующего файла всегда кратен 8 -2. Между данными файлов могут быть 0-7 байт нулевого padding -3. При чтении нужно использовать `packedSize`, а не выравнивать вручную - -### 3. Каталог файлов (Directory) - -Каталог находится в **конце файла**. Его расположение вычисляется по формуле: - -``` -DirectoryOffset = FileSize - (FileCount * 64) -``` - -Каждая запись в каталоге имеет **фиксированный размер 64 байта (0x40)**: - -```c -struct NResFileEntry { - char name[16]; // +0x00: Имя файла (NULL-terminated, uppercase) - uint32_t crc32; // +0x10: CRC32 хеш упакованных данных - uint32_t packMethod; // +0x14: Флаги метода упаковки (также используется как XOR seed) - uint32_t unpackedSize; // +0x18: Размер файла после распаковки - uint32_t packedSize; // +0x1C: Размер упакованных данных - uint32_t dataOffset; // +0x20: Смещение данных от начала файла - uint32_t fastDataPtr; // +0x24: Указатель для быстрого доступа (в памяти) - uint32_t xorSize; // +0x28: Размер данных для XOR-шифрования - uint32_t sortIndex; // +0x2C: Индекс для сортировки по имени - uint32_t reserved[4]; // +0x30: Зарезервировано (обычно нули) -}; -``` - -## Подробное описание полей каталога - -### Поле: name (смещение +0x00, 16 байт) - -- **Назначение**: Имя файла в архиве -- **Формат**: NULL-terminated строка, максимум 15 символов + NULL -- **Особенности**: - - Все символы хранятся в **UPPERCASE** (заглавными буквами) - - При поиске файлов используется регистронезависимое сравнение (`_strcmpi`) - - Если имя короче 16 байт, остаток заполняется нулями - -### Поле: crc32 (смещение +0x10, 4 байта) - -- **Назначение**: Контрольная сумма CRC32 упакованных данных -- **Использование**: Проверка целостности данных при чтении - -### Поле: packMethod (смещение +0x14, 4 байта) - -**Критически важное поле!** Содержит битовые флаги, определяющие метод обработки данных: - -```c -// Маски для извлечения метода упаковки -#define PACK_METHOD_MASK 0x1E0 // Биты 5-8 (метод + XOR) -#define PACK_METHOD_MASK2 0x1C0 // Биты 6-7 (без XOR-бита) - -// Методы упаковки (packMethod & 0x1E0) -#define PACK_NONE 0x000 // Нет упаковки (raw) -#define PACK_XOR 0x020 // XOR (только шифрование) -#define PACK_FRES 0x040 // FRES (LZSS простой режим) -#define PACK_FRES_XOR 0x060 // XOR + FRES -#define PACK_LZHUF 0x080 // LZHUF (LZSS + adaptive Huffman) -#define PACK_LZHUF_XOR 0x0A0 // XOR + LZHUF -#define PACK_DEFLATE_RAW 0x100 // RAW-DEFLATE (без zlib-обёртки) -``` - -**Алгоритм определения метода:** - -1. Извлечь биты `packMethod & 0x1E0` -2. Проверить конкретные значения: - - `0x000`: Данные не сжаты, простое копирование - - `0x020`: XOR-шифрование с двухбайтовым ключом - - `0x040` или `0x060`: FRES (может быть + XOR) - - `0x080` или `0x0A0`: LZHUF (может быть + XOR) - - `0x100`: RAW-DEFLATE (inflate без zlib-обёртки) - -**Важно:** `rsGetPackMethod()` возвращает `packMethod & 0x1C0`, то есть маску **без XOR-бита `0x20`**. Это нужно учитывать при сравнении. - -**Примечание про XOR seed:** значение для XOR берётся из поля `packMethod` (смещение `+0x14`). Это же поле может быть перезаписано при формировании каталога (см. раздел о `rsOpenLib`), если в библиотеке нет готовой таблицы сортировки. - -### Поле: unpackedSize (смещение +0x18, 4 байта) - -- **Назначение**: Размер файла после полной распаковки -- **Использование**: - - Для выделения памяти под распакованные данные - - Для проверки корректности распаковки - -### Поле: packedSize (смещение +0x1C, 4 байта) - -- **Назначение**: Размер сжатых данных в архиве -- **Особенности**: - - Если `packedSize == 0`, файл пустой или является указателем - - Для несжатых файлов: `packedSize == unpackedSize` - -### Поле: dataOffset (смещение +0x20, 4 байта) - -- **Назначение**: Абсолютное смещение данных файла от начала NRes файла -- **Формула вычисления**: `BaseAddress + dataOffset = начало данных` -- **Диапазон**: Обычно от 0x10 (после заголовка) до начала каталога - -### Поле: fastDataPtr (смещение +0x24, 4 байта) - -- **Назначение**: Указатель на данные в памяти для быстрого доступа -- **Использование**: Только во время выполнения (runtime) -- **В файле**: Обычно равно 0 или содержит относительный offset -- **Особенность**: Используется функцией `rsLoadFast()` для файлов без упаковки - -### Поле: xorSize (смещение +0x28, 4 байта) - -- **Назначение**: Размер данных для XOR-шифрования при комбинированных методах -- **Использование**: - - Когда `packMethod & 0x60 == 0x60` (FRES + XOR) - - Сначала применяется XOR к этому количеству байт, затем FRES к результату -- **Значение**: Может отличаться от `packedSize` при многоэтапной упаковке - -### Поле: sortIndex (смещение +0x2C, 4 байта) - -- **Назначение**: Индекс для быстрого поиска по отсортированному каталогу -- **Использование**: - - В `rsOpenLib` при отсутствии маркера `0xABBA` формируется таблица индексов сортировки имён - - Индексы записываются в это поле с шагом 0x40 (по записи) - - Используется `rsFind()` через таблицу индексов, а не прямую сортировку записей - -### Поле: reserved (смещение +0x30, 16 байт) - -- **Назначение**: Зарезервировано для будущих расширений -- **В файле**: Обычно заполнено нулями -- **Может содержать**: Дополнительные метаданные в новых версиях формата - -## Алгоритмы упаковки - -### 1. Без упаковки (PACK_NONE = 0x000) - -``` -Простое копирование данных: - memcpy(destination, source, packedSize); -``` - -### 2. XOR-шифрование (PACK_XOR = 0x020) - -```c -// Ключ/seed берется из поля packMethod (смещение +0x14) -uint16_t key = (uint16_t)(packMethod & 0xFFFF); - -for (int i = 0; i < packedSize; i++) { - uint8_t byte = source[i]; - destination[i] = byte ^ (key >> 8) ^ (key << 1); - - // Обновление ключа - uint8_t newByte = (key >> 8) ^ (key << 1); - key = (newByte ^ ((key >> 8) >> 1)) | (newByte << 8); -} -``` - -**Ключевые особенности:** - -- Используется 16-битный ключ из младших байт поля `packMethod` -- Ключ изменяется после каждого байта по специальному алгоритму -- Операции: XOR с старшим байтом ключа и со сдвинутым значением - -### 3. [FRES компрессия](fres_decompression.md) (PACK_FRES = 0x040, 0x060) - -Алгоритм FRES — это RLE-подобное сжатие с особой кодировкой повторов: - -``` -sub_1001B22E() - функция декомпрессии FRES - - Читает управляющие байты - - Декодирует литералы и повторы - - Использует скользящее окно для ссылок -``` - -### 4. [LZHUF (adaptive Huffman)](fres_decompression.md) (PACK_LZHUF = 0x080, 0x0A0) - -Наиболее сложный и эффективный метод: - -**Процесс декодирования:** - -1. Распаковка LZSS + adaptive Huffman (Okumura LZHUF) -2. Дерево обновляется после каждого символа -3. Match-символы преобразуются в длину и позицию - -### 5. [RAW-DEFLATE](huffman_decompression.md) (PACK_DEFLATE_RAW = 0x100) - -Это inflate без zlib-обёртки (без 2-байтового заголовка и Adler32). - -## Высокоуровневая инструкция по реализации - -### Этап 1: Открытие файла - -```python -def open_nres_file(filepath): - with open(filepath, 'rb') as f: - # 1. Читаем заголовок (16 байт) - header_data = f.read(16) - signature, version, file_count, file_size = struct.unpack('<4I', header_data) - - # 2. Проверяем сигнатуру - if signature != 0x7365524E: # "NRes" - raise ValueError("Неверная сигнатура файла") - - # 3. Проверяем версию - if version != 0x100: - raise ValueError(f"Неподдерживаемая версия: {version}") - - # 4. Вычисляем расположение каталога - directory_offset = file_size - (file_count * 64) - - # 5. Читаем весь файл в память (или используем memory mapping) - f.seek(0) - file_data = f.read() - - return { - 'file_count': file_count, - 'file_size': file_size, - 'directory_offset': directory_offset, - 'data': file_data - } -``` - -### Этап 2: Чтение каталога - -```python -def read_directory(nres_file): - data = nres_file['data'] - offset = nres_file['directory_offset'] - file_count = nres_file['file_count'] - - entries = [] - - for i in range(file_count): - entry_offset = offset + (i * 64) - entry_data = data[entry_offset:entry_offset + 64] - - # Парсим 64-байтовую запись - name = entry_data[0:16].decode('ascii').rstrip('\x00') - crc32, pack_method, unpacked_size, packed_size, data_offset, \ - fast_ptr, xor_size, sort_index = struct.unpack('<8I', entry_data[16:48]) - - entry = { - 'name': name, - 'crc32': crc32, - 'pack_method': pack_method, - 'unpacked_size': unpacked_size, - 'packed_size': packed_size, - 'data_offset': data_offset, - 'fast_data_ptr': fast_ptr, - 'xor_size': xor_size, - 'sort_index': sort_index - } - - entries.append(entry) - - return entries -``` - -### Этап 3: Поиск файла по имени - -```python -def find_file(entries, filename): - # Имена в архиве хранятся в UPPERCASE - search_name = filename.upper()[:15] - - # Используем бинарный поиск, так как каталог отсортирован - # Сортировка по sort_index восстанавливает алфавитный порядок - sorted_entries = sorted(entries, key=lambda e: e['sort_index']) - - left, right = 0, len(sorted_entries) - 1 - - while left <= right: - mid = (left + right) // 2 - mid_name = sorted_entries[mid]['name'] - - if mid_name == search_name: - return sorted_entries[mid] - elif mid_name < search_name: - left = mid + 1 - else: - right = mid - 1 - - return None -``` - -### Этап 4: Извлечение данных файла - -```python -def extract_file(nres_file, entry): - data = nres_file['data'] - - # 1. Получаем упакованные данные - packed_data = data[entry['data_offset']: - entry['data_offset'] + entry['packed_size']] - - # 2. Определяем метод упаковки - pack_method = entry['pack_method'] & 0x1E0 - - # 3. Распаковываем в зависимости от метода - if pack_method == 0x000: - # Без упаковки - return unpack_none(packed_data) - - elif pack_method == 0x020: - # XOR-шифрование - return unpack_xor(packed_data, entry['pack_method'], entry['unpacked_size']) - - elif pack_method == 0x040 or pack_method == 0x060: - # FRES компрессия (может быть с XOR) - if pack_method == 0x060: - # Сначала XOR - temp_data = unpack_xor(packed_data, entry['pack_method'], entry['xor_size']) - return unpack_fres(temp_data, entry['unpacked_size']) - else: - return unpack_fres(packed_data, entry['unpacked_size']) - - elif pack_method == 0x080 or pack_method == 0x0A0: - # LZHUF (может быть с XOR) - if pack_method == 0x0A0: - temp_data = unpack_xor(packed_data, entry['pack_method'], entry['xor_size']) - return unpack_lzhuf(temp_data, entry['unpacked_size']) - return unpack_lzhuf(packed_data, entry['unpacked_size']) - - elif pack_method == 0x100: - # RAW-DEFLATE - return unpack_deflate_raw(packed_data, entry['unpacked_size']) - - else: - raise ValueError(f"Неподдерживаемый метод упаковки: 0x{pack_method:X}") -``` - -### Этап 5: Реализация алгоритмов распаковки - -```python -def unpack_none(data): - """Без упаковки - просто возвращаем данные""" - return data - -def unpack_xor(data, pack_method, size): - """XOR-дешифрование с изменяющимся ключом""" - result = bytearray(size) - key = pack_method & 0xFFFF # Берем младшие 16 бит из поля packMethod - - for i in range(min(size, len(data))): - byte = data[i] - - # XOR операция - high_byte = (key >> 8) & 0xFF - shifted = (key << 1) & 0xFFFF - result[i] = byte ^ high_byte ^ (shifted & 0xFF) - - # Обновление ключа - new_byte = high_byte ^ (key << 1) - key = (new_byte ^ (high_byte >> 1)) | ((new_byte & 0xFF) << 8) - key &= 0xFFFF - - return bytes(result) - -def unpack_fres(data, unpacked_size): - """ - FRES декомпрессия - гибридный RLE+LZ77 алгоритм - Полная реализация в nres_decompression.py (класс FRESDecoder) - """ - from nres_decompression import FRESDecoder - decoder = FRESDecoder() - return decoder.decompress(data, unpacked_size) - -def unpack_lzhuf(data, unpacked_size): - """ - LZHUF (LZSS + adaptive Huffman) - Полная реализация в nres_decompression.py (класс LZHUDecoder) - """ - from nres_decompression import LZHUDecoder - decoder = LZHUDecoder() - return decoder.decompress(data, unpacked_size) - -def unpack_deflate_raw(data, unpacked_size): - """ - RAW-DEFLATE (inflate без zlib-обертки) - Полная реализация в nres_decompression.py (класс RawDeflateDecoder) - """ - from nres_decompression import RawDeflateDecoder - decoder = RawDeflateDecoder() - return decoder.decompress(data, unpacked_size) -``` - -### Этап 6: Извлечение всех файлов - -```python -def extract_all(nres_filepath, output_dir): - import os - - # 1. Открываем NRes файл - nres_file = open_nres_file(nres_filepath) - - # 2. Читаем каталог - entries = read_directory(nres_file) - - # 3. Создаем выходную директорию - os.makedirs(output_dir, exist_ok=True) - - # 4. Извлекаем каждый файл - for entry in entries: - print(f"Извлечение: {entry['name']}") - - try: - # Извлекаем данные - unpacked_data = extract_file(nres_file, entry) - - # Сохраняем в файл - output_path = os.path.join(output_dir, entry['name']) - with open(output_path, 'wb') as f: - f.write(unpacked_data) - - print(f" ✓ Успешно ({len(unpacked_data)} байт)") - - except Exception as e: - print(f" ✗ Ошибка: {e}") -``` - -## Поддерживаемые контейнеры - -### 1. NRes (MAGIC "NRes") - -- Открывается через `niOpenResFile/niOpenResInMem` -- Каталог находится в конце файла (см. структуру выше) - -### 2. rsLib / NL (MAGIC "NL") - -Отдельный формат контейнера, обрабатывается `rsOpenLib`: - -- В начале файла проверяется `*(_WORD*)buf == 0x4C4E` (ASCII "NL" в little-endian) -- `buf[3] == 1` — версия/маркер -- `buf[2]` — количество записей -- Каталог расположен с offset `0x20`, размер `0x20 * count` -- Каталог перед разбором дешифруется (байтовый XOR-поток) - -## Поиск по имени (rsFind) - -- Имя обрезается до 16 байт, `name[15] = 0` -- Приводится к верхнему регистру (`_strupr`) -- Поиск идёт по таблице индексов сортировки (значение хранится в поле `sortIndex`) -- Если в rsLib нет маркера `0xABBA`, таблица строится пузырьковой сортировкой и индексы записываются в поле записи - -## Особенности и важные замечания - -### 1. Порядок байт (Endianness) - -- **Все многобайтовые значения хранятся в Little-Endian порядке** -- При чтении используйте `struct.unpack('<...')` - -### 2. Сортировка каталога - -- Каталог файлов **отсортирован по имени файла** (алфавитный порядок) -- Поле `sortIndex` хранит оригинальный индекс до сортировки -- Это позволяет использовать бинарный поиск - -### 3. Регистр символов - -- Все имена файлов конвертируются в **UPPERCASE** (заглавные буквы) -- При поиске используйте регистронезависимое сравнение - -### 4. Memory Mapping - -- Оригинальный код использует `MapViewOfFile` для эффективной работы с большими файлами -- Рекомендуется использовать memory-mapped файлы для больших архивов - -### 5. Валидация данных - -- **Всегда проверяйте сигнатуру** перед обработкой -- **Проверяйте версию** формата -- **Проверяйте CRC32** после распаковки -- **Проверяйте размеры** (unpacked_size должен совпадать с результатом) - -### 6. Обработка ошибок - -- Файл может быть поврежден -- Метод упаковки может быть неподдерживаемым -- Данные могут быть частично зашифрованы - -### 7. Производительность - -- Для несжатых файлов (`packMethod & 0x1E0 == 0`) можно использовать прямое чтение -- Поле `fastDataPtr` может содержать кешированный указатель -- Используйте буферизацию при последовательном чтении - -### 8. Выравнивание данных - -- **Все данные файлов выравниваются по 8 байт** -- После каждого файла может быть 0-7 байт нулевого padding -- `dataOffset` следующего файла всегда кратен 8 -- При чтении используйте `packedSize` из записи, не вычисляйте выравнивание -- При создании архива добавляйте padding: `padding = ((size + 7) & ~7) - size` - -## Пример использования - -```python -# Открыть архив -nres = open_nres_file("resources.nres") - -# Прочитать каталог -entries = read_directory(nres) - -# Вывести список файлов -for entry in entries: - print(f"{entry['name']:20s} - {entry['unpacked_size']:8d} байт") - -# Найти конкретный файл -entry = find_file(entries, "texture.bmp") -if entry: - data = extract_file(nres, entry) - with open("extracted_texture.bmp", "wb") as f: - f.write(data) - -# Извлечь все файлы -extract_all("resources.nres", "./extracted/") -``` - -## Дополнительные функции - -### Проверка формата файла - -```python -def is_nres_file(filepath): - try: - with open(filepath, 'rb') as f: - signature = struct.unpack('<I', f.read(4))[0] - return signature == 0x7365524E - except: - return False -``` - -### Получение информации о файле - -```python -def get_file_info(entry): - pack_names = { - 0x000: "Без сжатия", - 0x020: "XOR", - 0x040: "FRES", - 0x060: "FRES+XOR", - 0x080: "LZHUF", - 0x0A0: "LZHUF+XOR", - 0x100: "RAW-DEFLATE" - } - - pack_method = entry['pack_method'] & 0x1E0 - pack_name = pack_names.get(pack_method, f"Неизвестный (0x{pack_method:X})") - - ratio = 100.0 * entry['packed_size'] / entry['unpacked_size'] if entry['unpacked_size'] > 0 else 0 - - return { - 'name': entry['name'], - 'size': entry['unpacked_size'], - 'packed': entry['packed_size'], - 'compression': pack_name, - 'ratio': f"{ratio:.1f}%", - 'crc32': f"0x{entry['crc32']:08X}" - } -``` - -## Заключение - -Формат NRes представляет собой эффективный архив с поддержкой множества методов сжатия. |
