diff options
| author | Valentin Popov <valentin@popov.link> | 2026-02-05 00:32:24 +0300 |
|---|---|---|
| committer | Valentin Popov <valentin@popov.link> | 2026-02-05 00:32:24 +0300 |
| commit | 40e7d88fd0684beaf91d9fc24e8b5a3639b30be2 (patch) | |
| tree | c9ad1b427040f7f1eabcd8516297a8ca7d991ebf /docs/specs/assets/nres | |
| parent | afe6b9a29b1f4b5f116a96fa42ee929e32e530e3 (diff) | |
| download | fparkan-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/assets/nres')
| -rw-r--r-- | docs/specs/assets/nres/fres_decompression.md | 426 | ||||
| -rw-r--r-- | docs/specs/assets/nres/huffman_decompression.md | 598 | ||||
| -rw-r--r-- | docs/specs/assets/nres/overview.md | 578 |
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 представляет собой эффективный архив с поддержкой множества методов сжатия. |
