aboutsummaryrefslogtreecommitdiff
path: root/docs/specs/assets/nres/overview.md
diff options
context:
space:
mode:
Diffstat (limited to 'docs/specs/assets/nres/overview.md')
-rw-r--r--docs/specs/assets/nres/overview.md578
1 files changed, 578 insertions, 0 deletions
diff --git a/docs/specs/assets/nres/overview.md b/docs/specs/assets/nres/overview.md
new file mode 100644
index 0000000..f455862
--- /dev/null
+++ b/docs/specs/assets/nres/overview.md
@@ -0,0 +1,578 @@
+# Документация по формату NRes
+
+## Обзор
+
+NRes — это формат контейнера ресурсов, используемый в игровом движке Nikita. Файл представляет собой архив, содержащий несколько упакованных файлов с метаданными и поддержкой различных методов сжатия.
+
+## Структура файла NRes
+
+### 1. Заголовок файла (16 байт)
+
+```c
+struct NResHeader {
+ uint32_t signature; // +0x00: Сигнатура "NRes" (0x7365526E в little-endian)
+ uint32_t version; // +0x04: Версия формата (0x00000100 = версия 1.0)
+ uint32_t fileCount; // +0x08: Количество файлов в архиве
+ uint32_t fileSize; // +0x0C: Общий размер файла в байтах
+};
+```
+
+**Детали:**
+
+- `signature`: Константа `0x7365526E` (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: Флаги метода упаковки и опции
+ 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 (основной метод)
+#define PACK_METHOD_MASK2 0x1C0 // Биты 6-7 (альтернативная маска)
+
+// Методы упаковки (биты 5-8)
+#define PACK_NONE 0x000 // Нет упаковки (копирование)
+#define PACK_XOR 0x020 // XOR-шифрование
+#define PACK_FRES 0x040 // FRES компрессия (устаревшая)
+#define PACK_FRES_XOR 0x060 // FRES + XOR (два прохода)
+#define PACK_ZLIB 0x080 // Zlib сжатие (устаревшее)
+#define PACK_ZLIB_XOR 0x0A0 // Zlib + XOR (два прохода)
+#define PACK_HUFFMAN 0x0E0 // Huffman кодирование (основной метод)
+
+// Дополнительные флаги
+#define FLAG_ENCRYPTED 0x040 // Файл зашифрован/требует декодирования
+```
+
+**Алгоритм определения метода:**
+
+1. Извлечь биты `packMethod & 0x1E0`
+2. Проверить конкретные значения:
+ - `0x000`: Данные не сжаты, простое копирование
+ - `0x020`: XOR-шифрование с двухбайтовым ключом
+ - `0x040` или `0x060`: FRES компрессия (может быть + XOR)
+ - `0x080` или `0x0A0`: Zlib компрессия (может быть + XOR)
+ - `0x0E0`: Huffman кодирование (наиболее распространенный)
+
+### Поле: 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 байта)
+
+- **Назначение**: Индекс для быстрого поиска по отсортированному каталогу
+- **Использование**:
+ - Каталог сортируется по алфавиту (имени файлов)
+ - `sortIndex` хранит оригинальный порядковый номер файла
+ - Позволяет использовать бинарный поиск для функции `rsFind()`
+
+### Поле: reserved (смещение +0x30, 16 байт)
+
+- **Назначение**: Зарезервировано для будущих расширений
+- **В файле**: Обычно заполнено нулями
+- **Может содержать**: Дополнительные метаданные в новых версиях формата
+
+## Алгоритмы упаковки
+
+### 1. Без упаковки (PACK_NONE = 0x000)
+
+```
+Простое копирование данных:
+ memcpy(destination, source, packedSize);
+```
+
+### 2. XOR-шифрование (PACK_XOR = 0x020)
+
+```c
+// Ключ берется из поля crc32
+uint16_t key = (uint16_t)(crc32 & 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-битный ключ из младших байт CRC32
+- Ключ изменяется после каждого байта по специальному алгоритму
+- Операции: XOR с старшим байтом ключа и со сдвинутым значением
+
+### 3. [FRES компрессия](fres_decompression.md) (PACK_FRES = 0x040, 0x060)
+
+Алгоритм FRES — это RLE-подобное сжатие с особой кодировкой повторов:
+
+```
+sub_1001B22E() - функция декомпрессии FRES
+ - Читает управляющие байты
+ - Декодирует литералы и повторы
+ - Использует скользящее окно для ссылок
+```
+
+### 4. [Huffman кодирование](huffman_decompression.md) (PACK_HUFFMAN = 0x0E0)
+
+Наиболее сложный и эффективный метод:
+
+```c
+// Структура декодера
+struct HuffmanDecoder {
+ uint32_t bitBuffer[0x4000]; // Буфер для битов
+ uint32_t compressedSize; // Размер сжатых данных
+ uint32_t outputPosition; // Текущая позиция в выходном буфере
+ uint32_t inputPosition; // Позиция в входных данных
+ uint8_t* sourceData; // Указатель на сжатые данные
+ uint8_t* destData; // Указатель на выходной буфер
+ uint32_t bitPosition; // Позиция бита в буфере
+ // ... дополнительные поля
+};
+```
+
+**Процесс декодирования:**
+
+1. Инициализация структуры декодера
+2. Чтение битов и построение дерева Huffman
+3. Декодирование символов по дереву
+4. Запись в выходной буфер
+
+## Высокоуровневая инструкция по реализации
+
+### Этап 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 != 0x7365526E: # "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()
+
+ # Используем бинарный поиск, так как каталог отсортирован
+ # Сортировка по 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['crc32'], 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['crc32'], entry['xor_size'])
+ return unpack_fres(temp_data, entry['unpacked_size'])
+ else:
+ return unpack_fres(packed_data, entry['unpacked_size'])
+
+ elif pack_method == 0x0E0:
+ # Huffman кодирование
+ return unpack_huffman(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, crc32, size):
+ """XOR-дешифрование с изменяющимся ключом"""
+ result = bytearray(size)
+ key = crc32 & 0xFFFF # Берем младшие 16 бит
+
+ 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_huffman(data, unpacked_size):
+ """
+ Huffman декодирование (DEFLATE-подобный)
+ Полная реализация в nres_decompression.py (класс HuffmanDecoder)
+ """
+ from nres_decompression import HuffmanDecoder
+ decoder = HuffmanDecoder()
+ 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. Порядок байт (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 == 0x7365526E
+ except:
+ return False
+```
+
+### Получение информации о файле
+
+```python
+def get_file_info(entry):
+ pack_names = {
+ 0x000: "Без сжатия",
+ 0x020: "XOR",
+ 0x040: "FRES",
+ 0x060: "FRES+XOR",
+ 0x080: "Zlib",
+ 0x0A0: "Zlib+XOR",
+ 0x0E0: "Huffman"
+ }
+
+ 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 представляет собой эффективный архив с поддержкой множества методов сжатия.