aboutsummaryrefslogtreecommitdiff
path: root/docs/specs
diff options
context:
space:
mode:
authorValentin Popov <valentin@popov.link>2026-02-05 00:32:24 +0300
committerValentin Popov <valentin@popov.link>2026-02-05 00:32:24 +0300
commit40e7d88fd0684beaf91d9fc24e8b5a3639b30be2 (patch)
treec9ad1b427040f7f1eabcd8516297a8ca7d991ebf /docs/specs
parentafe6b9a29b1f4b5f116a96fa42ee929e32e530e3 (diff)
downloadfparkan-40e7d88fd0684beaf91d9fc24e8b5a3639b30be2.tar.xz
fparkan-40e7d88fd0684beaf91d9fc24e8b5a3639b30be2.zip
Add NRes format documentation and decompression algorithms
- Created `huffman_decompression.md` detailing the Huffman decompression algorithm used in NRes, including context structure, block modes, and decoding methods. - Created `overview.md` for the NRes format, outlining file structure, header details, file entries, and packing algorithms. - Updated `mkdocs.yml` to include new documentation files in the navigation structure.
Diffstat (limited to 'docs/specs')
-rw-r--r--docs/specs/assets/nres/fres_decompression.md426
-rw-r--r--docs/specs/assets/nres/huffman_decompression.md598
-rw-r--r--docs/specs/assets/nres/overview.md578
3 files changed, 1602 insertions, 0 deletions
diff --git a/docs/specs/assets/nres/fres_decompression.md b/docs/specs/assets/nres/fres_decompression.md
new file mode 100644
index 0000000..1e0b894
--- /dev/null
+++ b/docs/specs/assets/nres/fres_decompression.md
@@ -0,0 +1,426 @@
+# FRES Декомпрессия
+
+## Обзор
+
+FRES — это гибридный алгоритм сжатия, использующий комбинацию RLE (Run-Length Encoding) и LZ77-подобного сжатия со скользящим окном. Существуют два режима работы: **adaptive Huffman** (флаг `a1 < 0`) и **простой битовый** (флаг `a1 >= 0`).
+
+```c
+char __stdcall sub_1001B22E(
+ char a1, // Флаг режима (< 0 = Huffman, >= 0 = простой)
+ int a2, // Ключ/seed (не используется напрямую)
+ _BYTE *a3, // Выходной буфер
+ int a4, // Размер выходного буфера
+ _BYTE *a5, // Входные сжатые данные
+ int a6 // Размер входных данных
+)
+```
+
+## Структуры данных
+
+### Глобальные переменные
+
+```c
+byte_1003A910[4096] // Циклический буфер скользящего окна (12 бит адрес)
+dword_1003E09C // Указатель на конец выходного буфера
+dword_1003E0A0 // Текущая позиция в циклическом буфере
+dword_1003E098 // Состояние Huffman дерева
+dword_1003E0A4 // Длина повтора для LZ77
+```
+
+### Константы
+
+```c
+#define WINDOW_SIZE 4096 // Размер скользящего окна (0x1000)
+#define WINDOW_MASK 0x0FFF // Маска для циклического буфера
+#define INIT_POS_NEG 4078 // Начальная позиция для Huffman режима
+#define INIT_POS_POS 4036 // Начальная позиция для простого режима
+```
+
+## Режим 1: Простой битовый режим (a1 >= 0)
+
+Это более простой режим без Huffman кодирования. Работает следующим образом:
+
+### Алгоритм
+
+```
+Инициализация:
+ position = 4036
+ flags = 0
+ flagBits = 0
+
+Цикл декомпрессии:
+ Пока есть входные данные и выходной буфер не заполнен:
+
+ 1. Прочитать бит флага:
+ if (flagBits высокий бит == 0):
+ flags = *input++
+ flagBits = 127 (0x7F)
+
+ flag_bit = flags & 1
+ flags >>= 1
+
+ 2. Прочитать второй бит:
+ if (flagBits низкий бит == 0):
+ загрузить новый байт флагов
+
+ second_bit = flags & 1
+ flags >>= 1
+
+ 3. Выбор действия по битам:
+
+ a) Если оба бита == 0:
+ // Литерал - копировать один байт
+ byte = *input++
+ window[position] = byte
+ *output++ = byte
+ position = (position + 1) & 0xFFF
+
+ b) Если второй бит == 0 (первый == 1):
+ // LZ77 копирование
+ word = *(uint16*)input
+ input += 2
+
+ offset = (word >> 4) & 0xFFF // 12 бит offset
+ length = (word & 0xF) + 3 // 4 бита длины + 3
+
+ src_pos = offset
+ Повторить length раз:
+ byte = window[src_pos]
+ window[position] = byte
+ *output++ = byte
+ src_pos = (src_pos + 1) & 0xFFF
+ position = (position + 1) & 0xFFF
+```
+
+### Формат сжатых данных (простой режим)
+
+```
+Битовый поток:
+
+[FLAG_BIT] [SECOND_BIT] [DATA]
+
+Где:
+ FLAG_BIT = 0, SECOND_BIT = 0: → Литерал (1 байт следует)
+ FLAG_BIT = 1, SECOND_BIT = 0: → LZ77 копирование (2 байта следуют)
+ FLAG_BIT = любой, SECOND_BIT = 1: → Литерал (1 байт следует)
+
+Формат LZ77 копирования (2 байта, little-endian):
+ Байт 0: offset_low (биты 0-7)
+ Байт 1: [length:4][offset_high:4]
+
+ offset = (byte1 >> 4) | (byte0 << 4) // 12 бит
+ length = (byte1 & 0x0F) + 3 // 4 бита + 3 = 3-18 байт
+```
+
+## Режим 2: Adaptive Huffman режим (a1 < 0)
+
+Более сложный режим с динамическим Huffman деревом.
+
+### Инициализация Huffman
+
+```c
+Инициализация таблиц:
+ 1. Создание таблицы быстрого декодирования (dword_1003B94C[256])
+ 2. Инициализация длин кодов (byte_1003BD4C[256])
+ 3. Построение начального дерева (627 узлов)
+```
+
+### Алгоритм декодирования
+
+```
+Инициализация:
+ position = 4078
+ bit_buffer = 0
+ bit_count = 8
+
+ Инициализировать окно значением 0x20 (пробел):
+ for i in range(2039):
+ window[i] = 0x20
+
+Цикл декомпрессии:
+ Пока не конец выходного буфера:
+
+ 1. Декодировать символ через Huffman дерево:
+
+ tree_index = dword_1003E098 // начальный узел
+
+ Пока tree_index < 627: // внутренний узел
+ bit = прочитать_бит()
+ tree_index = tree[tree_index + bit]
+
+ symbol = tree_index - 627 // лист дерева
+
+ Обновить дерево (sub_1001B0AE)
+
+ 2. Обработать символ:
+
+ if (symbol < 256):
+ // Литерал
+ window[position] = symbol
+ *output++ = symbol
+ position = (position + 1) & 0xFFF
+
+ else:
+ // LZ77 копирование
+ length = symbol - 253
+
+ // Прочитать offset (закодирован отдельно)
+ offset_bits = прочитать_биты(таблица длин)
+ offset = декодировать(offset_bits)
+
+ src_pos = (position - 1 - offset) & 0xFFF
+
+ Повторить length раз:
+ byte = window[src_pos]
+ window[position] = byte
+ *output++ = byte
+ src_pos = (src_pos + 1) & 0xFFF
+ position = (position + 1) & 0xFFF
+```
+
+### Обновление дерева
+
+Адаптивное Huffman дерево обновляется после каждого декодированного символа:
+
+```
+Алгоритм обновления:
+ 1. Увеличить счетчик частоты символа
+ 2. Если частота превысила порог:
+ Перестроить узлы дерева (swapping)
+ 3. Если счетчик достиг 0x8000:
+ Пересчитать все частоты (разделить на 2)
+```
+
+## Псевдокод полной реализации
+
+### Декодер (простой режим)
+
+```python
+def fres_decompress_simple(input_data, output_size):
+ """
+ FRES декомпрессия в простом режиме
+ """
+ # Инициализация
+ window = bytearray(4096)
+ position = 4036
+ output = bytearray()
+
+ input_pos = 0
+ flags = 0
+ flag_bits_high = 0
+ flag_bits_low = 0
+
+ while len(output) < output_size and input_pos < len(input_data):
+ # Читаем флаг высокого бита
+ if (flag_bits_high & 1) == 0:
+ if input_pos >= len(input_data):
+ break
+ flags = input_data[input_pos]
+ input_pos += 1
+ flag_bits_high = 127 # 0x7F
+
+ flag_high = flag_bits_high & 1
+ flag_bits_high >>= 1
+
+ # Читаем флаг низкого бита
+ if input_pos >= len(input_data):
+ break
+
+ if (flag_bits_low & 1) == 0:
+ flags = input_data[input_pos]
+ input_pos += 1
+ flag_bits_low = 127
+
+ flag_low = flags & 1
+ flags >>= 1
+
+ # Обработка по флагам
+ if not flag_low: # Второй бит == 0
+ if not flag_high: # Оба бита == 0
+ # Литерал
+ if input_pos >= len(input_data):
+ break
+ byte = input_data[input_pos]
+ input_pos += 1
+
+ window[position] = byte
+ output.append(byte)
+ position = (position + 1) & 0xFFF
+
+ else: # Первый == 1, второй == 0
+ # LZ77 копирование
+ if input_pos + 1 >= len(input_data):
+ break
+
+ word = input_data[input_pos] | (input_data[input_pos + 1] << 8)
+ input_pos += 2
+
+ offset = (word >> 4) & 0xFFF
+ length = (word & 0xF) + 3
+
+ for _ in range(length):
+ if len(output) >= output_size:
+ break
+
+ byte = window[offset]
+ window[position] = byte
+ output.append(byte)
+
+ offset = (offset + 1) & 0xFFF
+ position = (position + 1) & 0xFFF
+
+ else: # Второй бит == 1
+ # Литерал
+ if input_pos >= len(input_data):
+ break
+ byte = input_data[input_pos]
+ input_pos += 1
+
+ window[position] = byte
+ output.append(byte)
+ position = (position + 1) & 0xFFF
+
+ return bytes(output[:output_size])
+```
+
+### Вспомогательные функции
+
+```python
+class BitReader:
+ """Класс для побитового чтения"""
+
+ def __init__(self, data):
+ self.data = data
+ self.pos = 0
+ self.bit_buffer = 0
+ self.bits_available = 0
+
+ def read_bit(self):
+ """Прочитать один бит"""
+ if self.bits_available == 0:
+ if self.pos >= len(self.data):
+ return 0
+ self.bit_buffer = self.data[self.pos]
+ self.pos += 1
+ self.bits_available = 8
+
+ bit = self.bit_buffer & 1
+ self.bit_buffer >>= 1
+ self.bits_available -= 1
+ return bit
+
+ def read_bits(self, count):
+ """Прочитать несколько бит"""
+ result = 0
+ for i in range(count):
+ result |= self.read_bit() << i
+ return result
+
+
+def initialize_window():
+ """Инициализация окна для Huffman режима"""
+ window = bytearray(4096)
+ # Заполняем начальным значением
+ for i in range(4078):
+ window[i] = 0x20 # Пробел
+ return window
+```
+
+## Ключевые особенности
+
+### 1. Циклический буфер
+
+- Размер: 4096 байт (12 бит адресации)
+- Маска: `0xFFF` для циклического доступа
+- Начальная позиция зависит от режима
+
+### 2. Dual-режимы
+
+- **Простой**: Быстрее, меньше сжатие, для данных с низкой энтропией
+- **Huffman**: Медленнее, лучше сжатие, для данных с высокой энтропией
+
+### 3. LZ77 кодирование
+
+- Offset: 12 бит (0-4095)
+- Length: 4 бита + 3 (3-18 байт)
+- Максимальное копирование: 18 байт
+
+### 4. Битовые флаги
+
+Используется двойная система флагов для определения типа следующих данных
+
+## Проблемы реализации
+
+### 1. Битовый порядок
+
+Биты читаются справа налево (LSB first), что может вызвать путаницу
+
+### 2. Huffman дерево
+
+Адаптивное дерево требует точного отслеживания частот и правильной перестройки
+
+### 3. Граничные условия
+
+Необходимо тщательно проверять границы буферов
+
+## Примеры данных
+
+### Пример 1: Литералы (простой режим)
+
+```
+Входные биты: 00 00 00 ...
+Выход: Последовательность литералов
+
+Пример:
+ Flags: 0x00 (00000000)
+ Data: 0x41 ('A'), 0x42 ('B'), 0x43 ('C'), ...
+ Выход: "ABC..."
+```
+
+### Пример 2: LZ77 копирование
+
+```
+Входные биты: 10 ...
+Выход: Копирование из окна
+
+Пример:
+ Flags: 0x01 (00000001) - первый бит = 1
+ Word: 0x1234
+
+ Разбор:
+ offset = (0x34 << 4) | (0x12 >> 4) = 0x341
+ length = (0x12 & 0xF) + 3 = 5
+
+ Действие: Скопировать 5 байт с позиции offset
+```
+
+## Отладка
+
+Для отладки рекомендуется:
+
+```python
+def debug_fres_decompress(input_data, output_size):
+ """Версия с отладочным выводом"""
+ print(f"Input size: {len(input_data)}")
+ print(f"Output size: {output_size}")
+
+ # ... реализация с print на каждом шаге
+
+ print(f"Flag: {flag_high}{flag_low}")
+ if is_literal:
+ print(f" Literal: 0x{byte:02X}")
+ else:
+ print(f" LZ77: offset={offset}, length={length}")
+```
+
+## Заключение
+
+FRES — это эффективный гибридный алгоритм, сочетающий:
+
+- RLE для повторяющихся данных
+- LZ77 для ссылок на предыдущие данные
+- Опциональный Huffman для символов
+
+**Сложность декомпрессии:** O(n) где n — размер выходных данных
+
+**Размер окна:** 4 КБ — хороший баланс между памятью и степенью сжатия
diff --git a/docs/specs/assets/nres/huffman_decompression.md b/docs/specs/assets/nres/huffman_decompression.md
new file mode 100644
index 0000000..aafc02b
--- /dev/null
+++ b/docs/specs/assets/nres/huffman_decompression.md
@@ -0,0 +1,598 @@
+# Huffman Декомпрессия
+
+## Обзор
+
+Это реализация **DEFLATE-подобного** алгоритма декомпрессии, используемого в [NRes](overview.md). Алгоритм поддерживает три режима блоков и использует два Huffman дерева для кодирования литералов/длин и расстояний.
+
+```c
+int __thiscall sub_1001AF10(
+ unsigned int *this, // Контекст декодера (HuffmanContext)
+ int *a2 // Выходной параметр (результат операции)
+)
+```
+
+## Структура контекста (HuffmanContext)
+
+```c
+struct HuffmanContext {
+ uint32_t bitBuffer[0x4000]; // 0x00000-0x0FFFF: Битовый буфер (32KB)
+ uint32_t compressedSize; // 0x10000: Размер сжатых данных
+ uint32_t unknown1; // 0x10004: Не используется
+ uint32_t outputPosition; // 0x10008: Позиция в выходном буфере
+ uint32_t currentByte; // 0x1000C: Текущий байт
+ uint8_t* sourceData; // 0x10010: Указатель на сжатые данные
+ uint8_t* destData; // 0x10014: Указатель на выходной буфер
+ uint32_t bitPosition; // 0x10018: Позиция бита
+ uint32_t inputPosition; // 0x1001C: Позиция чтения (this[16389])
+ uint32_t decodedBytes; // 0x10020: Декодированные байты (this[16386])
+ uint32_t bitBufferValue; // 0x10024: Значение бит буфера (this[16391])
+ uint32_t bitsAvailable; // 0x10028: Доступные биты (this[16392])
+ // ...
+};
+
+// Смещения в структуре:
+#define CTX_OUTPUT_POS 16385 // this[16385]
+#define CTX_DECODED_BYTES 16386 // this[16386]
+#define CTX_SOURCE_PTR 16387 // this[16387]
+#define CTX_DEST_PTR 16388 // this[16388]
+#define CTX_INPUT_POS 16389 // this[16389]
+#define CTX_BIT_BUFFER 16391 // this[16391]
+#define CTX_BITS_COUNT 16392 // this[16392]
+#define CTX_MAX_SYMBOL 16393 // this[16393]
+```
+
+## Три режима блоков
+
+Алгоритм определяет тип блока по первым 3 битам:
+
+```
+Биты: [TYPE:2] [FINAL:1]
+
+FINAL = 1: Это последний блок
+TYPE:
+ 00 = Несжатый блок (сырые данные)
+ 01 = Сжатый с фиксированными Huffman кодами
+ 10 = Сжатый с динамическими Huffman кодами
+ 11 = Зарезервировано (ошибка)
+```
+
+### Основной цикл декодирования
+
+```c
+int decode_block(HuffmanContext* ctx) {
+ // Читаем первый бит (FINAL)
+ int final_bit = read_bit(ctx);
+
+ // Читаем 2 бита (TYPE)
+ int type = read_bits(ctx, 2);
+
+ switch (type) {
+ case 0: // 00 - Несжатый блок
+ return decode_uncompressed_block(ctx);
+
+ case 1: // 01 - Фиксированные Huffman коды
+ return decode_fixed_huffman_block(ctx);
+
+ case 2: // 10 - Динамические Huffman коды
+ return decode_dynamic_huffman_block(ctx);
+
+ case 3: // 11 - Ошибка
+ return 2; // Неподдерживаемый тип
+ }
+
+ return final_bit ? 0 : 1; // 0 = конец, 1 = есть еще блоки
+}
+```
+
+## Режим 0: Несжатый блок
+
+Простое копирование байтов без сжатия.
+
+### Алгоритм
+
+```python
+def decode_uncompressed_block(ctx):
+ """
+ Формат несжатого блока:
+ [LEN:16][NLEN:16][DATA:LEN]
+
+ Где:
+ LEN - длина данных (little-endian)
+ NLEN - инверсия LEN (~LEN)
+ DATA - сырые данные
+ """
+ # Выравнивание к границе байта
+ bits_to_skip = ctx.bits_available & 7
+ ctx.bit_buffer >>= bits_to_skip
+ ctx.bits_available -= bits_to_skip
+
+ # Читаем длину (16 бит)
+ length = read_bits(ctx, 16)
+
+ # Читаем инверсию длины (16 бит)
+ nlength = read_bits(ctx, 16)
+
+ # Проверка целостности
+ if length != (~nlength & 0xFFFF):
+ return 1 # Ошибка
+
+ # Копируем данные
+ for i in range(length):
+ byte = read_byte(ctx)
+ write_output_byte(ctx, byte)
+
+ # Проверка переполнения выходного буфера
+ if ctx.output_position >= 0x8000:
+ flush_output_buffer(ctx)
+
+ return 0
+```
+
+### Детали
+
+- Данные копируются "как есть"
+- Используется для несжимаемых данных
+- Требует выравнивания по байтам перед чтением длины
+
+## Режим 1: Фиксированные Huffman коды
+
+Использует предопределенные Huffman таблицы.
+
+### Фиксированные таблицы длин кодов
+
+```python
+# Таблица для литералов/длин (288 символов)
+FIXED_LITERAL_LENGTHS = [
+ 8, 8, 8, 8, ..., 8, # 0-143: коды длины 8 (144 символа)
+ 9, 9, 9, 9, ..., 9, # 144-255: коды длины 9 (112 символов)
+ 7, 7, 7, 7, ..., 7, # 256-279: коды длины 7 (24 символа)
+ 8, 8, 8, 8, ..., 8 # 280-287: коды длины 8 (8 символов)
+]
+
+# Таблица для расстояний (30 символов)
+FIXED_DISTANCE_LENGTHS = [
+ 5, 5, 5, 5, ..., 5 # 0-29: все коды длины 5
+]
+```
+
+### Алгоритм декодирования
+
+```python
+def decode_fixed_huffman_block(ctx):
+ """Декодирование блока с фиксированными Huffman кодами"""
+
+ # Инициализация фиксированных таблиц
+ lit_tree = build_huffman_tree(FIXED_LITERAL_LENGTHS)
+ dist_tree = build_huffman_tree(FIXED_DISTANCE_LENGTHS)
+
+ while True:
+ # Декодировать символ литерала/длины
+ symbol = decode_huffman_symbol(ctx, lit_tree)
+
+ if symbol < 256:
+ # Литерал - просто вывести байт
+ write_output_byte(ctx, symbol)
+
+ elif symbol == 256:
+ # Конец блока
+ break
+
+ else:
+ # Символ длины (257-285)
+ length = decode_length(ctx, symbol)
+
+ # Декодировать расстояние
+ dist_symbol = decode_huffman_symbol(ctx, dist_tree)
+ distance = decode_distance(ctx, dist_symbol)
+
+ # Скопировать из истории
+ copy_from_history(ctx, distance, length)
+```
+
+### Таблицы экстра-бит
+
+```python
+# Дополнительные биты для длины
+LENGTH_EXTRA_BITS = {
+ 257: 0, 258: 0, 259: 0, 260: 0, 261: 0, 262: 0, 263: 0, 264: 0, # 3-10
+ 265: 1, 266: 1, 267: 1, 268: 1, # 11-18
+ 269: 2, 270: 2, 271: 2, 272: 2, # 19-34
+ 273: 3, 274: 3, 275: 3, 276: 3, # 35-66
+ 277: 4, 278: 4, 279: 4, 280: 4, # 67-130
+ 281: 5, 282: 5, 283: 5, 284: 5, # 131-257
+ 285: 0 # 258
+}
+
+LENGTH_BASE = {
+ 257: 3, 258: 4, 259: 5, ..., 285: 258
+}
+
+# Дополнительные биты для расстояния
+DISTANCE_EXTRA_BITS = {
+ 0: 0, 1: 0, 2: 0, 3: 0, # 1-4
+ 4: 1, 5: 1, 6: 2, 7: 2, # 5-12
+ 8: 3, 9: 3, 10: 4, 11: 4, # 13-48
+ 12: 5, 13: 5, 14: 6, 15: 6, # 49-192
+ 16: 7, 17: 7, 18: 8, 19: 8, # 193-768
+ 20: 9, 21: 9, 22: 10, 23: 10, # 769-3072
+ 24: 11, 25: 11, 26: 12, 27: 12, # 3073-12288
+ 28: 13, 29: 13 # 12289-24576
+}
+
+DISTANCE_BASE = {
+ 0: 1, 1: 2, 2: 3, 3: 4, ..., 29: 24577
+}
+```
+
+### Декодирование длины и расстояния
+
+```python
+def decode_length(ctx, symbol):
+ """Декодировать длину из символа"""
+ base = LENGTH_BASE[symbol]
+ extra_bits = LENGTH_EXTRA_BITS[symbol]
+
+ if extra_bits > 0:
+ extra = read_bits(ctx, extra_bits)
+ return base + extra
+
+ return base
+
+
+def decode_distance(ctx, symbol):
+ """Декодировать расстояние из символа"""
+ base = DISTANCE_BASE[symbol]
+ extra_bits = DISTANCE_EXTRA_BITS[symbol]
+
+ if extra_bits > 0:
+ extra = read_bits(ctx, extra_bits)
+ return base + extra
+
+ return base
+```
+
+## Режим 2: Динамические Huffman коды
+
+Самый сложный режим. Huffman таблицы передаются в начале блока.
+
+### Формат заголовка динамического блока
+
+```
+Биты заголовка:
+ [HLIT:5] - Количество литерал/длина кодов - 257 (значение: 257-286)
+ [HDIST:5] - Количество расстояние кодов - 1 (значение: 1-30)
+ [HCLEN:4] - Количество длин кодов для code length алфавита - 4 (значение: 4-19)
+
+Далее идут длины кодов для code length алфавита:
+ [CL0:3] [CL1:3] ... [CL(HCLEN-1):3]
+
+Порядок code length кодов:
+ 16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15
+```
+
+### Алгоритм декодирования
+
+```python
+def decode_dynamic_huffman_block(ctx):
+ """Декодирование блока с динамическими Huffman кодами"""
+
+ # 1. Читаем заголовок
+ hlit = read_bits(ctx, 5) + 257 # Количество литерал/длина кодов
+ hdist = read_bits(ctx, 5) + 1 # Количество расстояние кодов
+ hclen = read_bits(ctx, 4) + 4 # Количество code length кодов
+
+ if hlit > 286 or hdist > 30:
+ return 1 # Ошибка
+
+ # 2. Читаем длины для code length алфавита
+ CODE_LENGTH_ORDER = [16, 17, 18, 0, 8, 7, 9, 6, 10, 5,
+ 11, 4, 12, 3, 13, 2, 14, 1, 15]
+
+ code_length_lengths = [0] * 19
+ for i in range(hclen):
+ code_length_lengths[CODE_LENGTH_ORDER[i]] = read_bits(ctx, 3)
+
+ # 3. Строим дерево для code length
+ cl_tree = build_huffman_tree(code_length_lengths)
+
+ # 4. Декодируем длины литерал/длина и расстояние кодов
+ lengths = decode_code_lengths(ctx, cl_tree, hlit + hdist)
+
+ # 5. Разделяем на два алфавита
+ literal_lengths = lengths[:hlit]
+ distance_lengths = lengths[hlit:]
+
+ # 6. Строим деревья для декодирования
+ lit_tree = build_huffman_tree(literal_lengths)
+ dist_tree = build_huffman_tree(distance_lengths)
+
+ # 7. Декодируем данные (аналогично фиксированному режиму)
+ return decode_huffman_data(ctx, lit_tree, dist_tree)
+```
+
+### Декодирование длин кодов
+
+Используется специальный алфавит с тремя специальными символами:
+
+```python
+def decode_code_lengths(ctx, cl_tree, total_count):
+ """
+ Декодирование последовательности длин кодов
+
+ Специальные символы:
+ 16 - Повторить предыдущую длину 3-6 раз (2 доп. бита)
+ 17 - Повторить 0 длину 3-10 раз (3 доп. бита)
+ 18 - Повторить 0 длину 11-138 раз (7 доп. бит)
+ """
+ lengths = []
+ last_length = 0
+
+ while len(lengths) < total_count:
+ symbol = decode_huffman_symbol(ctx, cl_tree)
+
+ if symbol < 16:
+ # Обычная длина (0-15)
+ lengths.append(symbol)
+ last_length = symbol
+
+ elif symbol == 16:
+ # Повторить предыдущую длину
+ repeat = read_bits(ctx, 2) + 3
+ lengths.extend([last_length] * repeat)
+
+ elif symbol == 17:
+ # Повторить ноль (короткий)
+ repeat = read_bits(ctx, 3) + 3
+ lengths.extend([0] * repeat)
+ last_length = 0
+
+ elif symbol == 18:
+ # Повторить ноль (длинный)
+ repeat = read_bits(ctx, 7) + 11
+ lengths.extend([0] * repeat)
+ last_length = 0
+
+ return lengths[:total_count]
+```
+
+## Построение Huffman дерева
+
+```python
+def build_huffman_tree(code_lengths):
+ """
+ Построить Huffman дерево из длин кодов
+
+ Использует алгоритм "canonical Huffman codes"
+ """
+ max_length = max(code_lengths) if code_lengths else 0
+
+ # 1. Подсчитать количество кодов каждой длины
+ bl_count = [0] * (max_length + 1)
+ for length in code_lengths:
+ if length > 0:
+ bl_count[length] += 1
+
+ # 2. Вычислить первый код для каждой длины
+ code = 0
+ next_code = [0] * (max_length + 1)
+
+ for bits in range(1, max_length + 1):
+ code = (code + bl_count[bits - 1]) << 1
+ next_code[bits] = code
+
+ # 3. Присвоить числовые коды символам
+ tree = {}
+ for symbol, length in enumerate(code_lengths):
+ if length > 0:
+ tree[symbol] = {
+ 'code': next_code[length],
+ 'length': length
+ }
+ next_code[length] += 1
+
+ # 4. Создать структуру быстрого поиска
+ lookup_table = create_lookup_table(tree)
+
+ return lookup_table
+
+
+def decode_huffman_symbol(ctx, tree):
+ """Декодировать один символ из Huffman дерева"""
+ code = 0
+ length = 0
+
+ for length in range(1, 16):
+ bit = read_bit(ctx)
+ code = (code << 1) | bit
+
+ # Проверить в таблице быстрого поиска
+ if (code, length) in tree:
+ return tree[(code, length)]
+
+ return -1 # Ошибка декодирования
+```
+
+## Управление выходным буфером
+
+```python
+def write_output_byte(ctx, byte):
+ """Записать байт в выходной буфер"""
+ # Записываем в bitBuffer (используется как циклический буфер)
+ ctx.bitBuffer[ctx.decodedBytes] = byte
+ ctx.decodedBytes += 1
+
+ # Если буфер заполнен (32KB)
+ if ctx.decodedBytes >= 0x8000:
+ flush_output_buffer(ctx)
+
+
+def flush_output_buffer(ctx):
+ """Сбросить выходной буфер в финальный выход"""
+ # Копируем данные в финальный выходной буфер
+ dest_offset = ctx.outputPosition + ctx.destData
+ memcpy(dest_offset, ctx.bitBuffer, ctx.decodedBytes)
+
+ # Обновляем счетчики
+ ctx.outputPosition += ctx.decodedBytes
+ ctx.decodedBytes = 0
+
+
+def copy_from_history(ctx, distance, length):
+ """Скопировать данные из истории (LZ77)"""
+ # Позиция источника в циклическом буфере
+ src_pos = (ctx.decodedBytes - distance) & 0x7FFF
+
+ for i in range(length):
+ byte = ctx.bitBuffer[src_pos]
+ write_output_byte(ctx, byte)
+ src_pos = (src_pos + 1) & 0x7FFF
+```
+
+## Полная реализация на Python
+
+```python
+class HuffmanDecoder:
+ """Полный DEFLATE-подобный декодер"""
+
+ def __init__(self, input_data, output_size):
+ self.input_data = input_data
+ self.output_size = output_size
+ self.input_pos = 0
+ self.bit_buffer = 0
+ self.bits_available = 0
+ self.output = bytearray()
+ self.history = bytearray(32768) # 32KB циклический буфер
+ self.history_pos = 0
+
+ def read_bit(self):
+ """Прочитать один бит"""
+ if self.bits_available == 0:
+ if self.input_pos >= len(self.input_data):
+ return 0
+ self.bit_buffer = self.input_data[self.input_pos]
+ self.input_pos += 1
+ self.bits_available = 8
+
+ bit = self.bit_buffer & 1
+ self.bit_buffer >>= 1
+ self.bits_available -= 1
+ return bit
+
+ def read_bits(self, count):
+ """Прочитать несколько бит (LSB first)"""
+ result = 0
+ for i in range(count):
+ result |= self.read_bit() << i
+ return result
+
+ def write_byte(self, byte):
+ """Записать байт в выход и историю"""
+ self.output.append(byte)
+ self.history[self.history_pos] = byte
+ self.history_pos = (self.history_pos + 1) & 0x7FFF
+
+ def copy_from_history(self, distance, length):
+ """Скопировать из истории"""
+ src_pos = (self.history_pos - distance) & 0x7FFF
+
+ for _ in range(length):
+ byte = self.history[src_pos]
+ self.write_byte(byte)
+ src_pos = (src_pos + 1) & 0x7FFF
+
+ def decompress(self):
+ """Основной цикл декомпрессии"""
+ while len(self.output) < self.output_size:
+ # Читаем заголовок блока
+ final = self.read_bit()
+ block_type = self.read_bits(2)
+
+ if block_type == 0:
+ # Несжатый блок
+ if not self.decode_uncompressed_block():
+ break
+ elif block_type == 1:
+ # Фиксированные Huffman коды
+ if not self.decode_fixed_huffman_block():
+ break
+ elif block_type == 2:
+ # Динамические Huffman коды
+ if not self.decode_dynamic_huffman_block():
+ break
+ else:
+ # Ошибка
+ raise ValueError("Invalid block type")
+
+ if final:
+ break
+
+ return bytes(self.output[:self.output_size])
+
+ # ... реализации decode_*_block методов ...
+```
+
+## Оптимизации
+
+### 1. Таблица быстрого поиска
+
+```python
+# Предвычисленная таблица для 9 бит (первый уровень)
+FAST_LOOKUP_BITS = 9
+fast_table = [None] * (1 << FAST_LOOKUP_BITS)
+
+# Заполнение таблицы при построении дерева
+for symbol, info in tree.items():
+ if info['length'] <= FAST_LOOKUP_BITS:
+ # Все возможные префиксы для этого кода
+ code = info['code']
+ for i in range(1 << (FAST_LOOKUP_BITS - info['length'])):
+ lookup_code = code | (i << info['length'])
+ fast_table[lookup_code] = symbol
+```
+
+### 2. Буферизация битов
+
+```python
+# Читать по 32 бита за раз вместо побитового чтения
+def refill_bits(self):
+ """Пополнить битовый буфер"""
+ while self.bits_available < 24 and self.input_pos < len(self.input_data):
+ byte = self.input_data[self.input_pos]
+ self.input_pos += 1
+ self.bit_buffer |= byte << self.bits_available
+ self.bits_available += 8
+```
+
+## Отладка и тестирование
+
+```python
+def debug_huffman_decode(data):
+ """Декодирование с отладочной информацией"""
+ decoder = HuffmanDecoder(data, len(data) * 10)
+
+ original_read_bits = decoder.read_bits
+ def debug_read_bits(count):
+ result = original_read_bits(count)
+ print(f"Read {count} bits: 0x{result:0{count//4}X} ({result})")
+ return result
+
+ decoder.read_bits = debug_read_bits
+ return decoder.decompress()
+```
+
+## Заключение
+
+Этот Huffman декодер реализует **DEFLATE**-совместимый алгоритм с тремя режимами блоков:
+
+1. **Несжатый** - для несжимаемых данных
+2. **Фиксированный Huffman** - быстрое декодирование с предопределенными таблицами
+3. **Динамический Huffman** - максимальное сжатие с пользовательскими таблицами
+
+**Ключевые особенности:**
+
+- Поддержка LZ77 для повторяющихся последовательностей
+- Канонические Huffman коды для эффективного декодирования
+- Циклический буфер 32KB для истории
+- Оптимизации через таблицы быстрого поиска
+
+**Сложность:** O(n) где n - размер выходных данных
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 представляет собой эффективный архив с поддержкой множества методов сжатия.