From 54c94fddb5fcf4e38bc9124be0e9cec93a4cdcba Mon Sep 17 00:00:00 2001 From: Valentin Popov Date: Tue, 10 Feb 2026 00:30:25 +0400 Subject: Add detailed documentation for NRes and RsLi resource formats - Introduced a comprehensive markdown file `nres.md` detailing the structure, header, and operations of the NRes and RsLi formats. - Updated `mkdocs.yml` to reflect the new documentation structure, consolidating NRes and RsLi under a single entry. --- docs/specs/assets/nres/fres_decompression.md | 402 ---------------- docs/specs/assets/nres/huffman_decompression.md | 605 ----------------------- docs/specs/assets/nres/overview.md | 608 ------------------------ 3 files changed, 1615 deletions(-) delete mode 100644 docs/specs/assets/nres/fres_decompression.md delete mode 100644 docs/specs/assets/nres/huffman_decompression.md delete mode 100644 docs/specs/assets/nres/overview.md (limited to 'docs/specs/assets') diff --git a/docs/specs/assets/nres/fres_decompression.md b/docs/specs/assets/nres/fres_decompression.md deleted file mode 100644 index 6c73f66..0000000 --- a/docs/specs/assets/nres/fres_decompression.md +++ /dev/null @@ -1,402 +0,0 @@ -# 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. Прочитать бит флага (LSB-first): - if (flagBits == 0): - flags = *input++ - flagBits = 8 - - flag_bit = flags & 1 - flags >>= 1 - flagBits -= 1 - - 2. Выбор действия по биту: - - a) Если bit == 1: - // Литерал - копировать один байт - byte = *input++ - window[position] = byte - *output++ = byte - position = (position + 1) & 0xFFF - - b) Если bit == 0: - // LZ77 копирование (2 байта) - word = *(uint16*)input - input += 2 - - b0 = word & 0xFF - b1 = (word >> 8) & 0xFF - - offset = b0 | ((b1 & 0xF0) << 4) // 12 бит offset - length = (b1 & 0x0F) + 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] [DATA] - -Где: - FLAG_BIT = 1: → Литерал (1 байт следует) - FLAG_BIT = 0: → LZ77 копирование (2 байта следуют) - -Формат LZ77 копирования (2 байта, little-endian): - Байт 0: offset_low (биты 0-7) - Байт 1: [length:4][offset_high:4] - - offset = byte0 | ((byte1 & 0xF0) << 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 узлов, T = 2*N_CHAR - 1) - где N_CHAR = 314 (256 литералов + 58 кодов длины) -``` - -### Алгоритм декодирования - -``` -Инициализация: - 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: - // LZSS копирование (LZHUF) - length = symbol - 253 // 3..60 - match_pos = decode_position() // префикс + 6 бит - - src_pos = (position - 1 - match_pos) & 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 = 0 - - while len(output) < output_size and input_pos < len(input_data): - # Читаем флаг (LSB-first) - if flag_bits == 0: - if input_pos >= len(input_data): - break - flags = input_data[input_pos] - input_pos += 1 - flag_bits = 8 - - flag = flags & 1 - flags >>= 1 - flag_bits -= 1 - - # Обработка по флагу - if flag: # 1 = literal - # Литерал - 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: # 0 = backref (2 байта) - if input_pos + 1 >= len(input_data): - break - - b0 = input_data[input_pos] - b1 = input_data[input_pos + 1] - input_pos += 2 - - offset = b0 | ((b1 & 0xF0) << 4) - length = (b1 & 0x0F) + 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 - - 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. Битовые флаги - -Используется один флаговый бит (LSB-first) для определения типа данных: - -- `1` → literal (1 байт) -- `0` → backref (2 байта) - -## Проблемы реализации - -### 1. Битовый порядок - -Биты читаются справа налево (LSB first), что может вызвать путаницу - -### 2. Huffman дерево - -Адаптивное дерево требует точного отслеживания частот и правильной перестройки - -### 3. Граничные условия - -Необходимо тщательно проверять границы буферов - -- В простом режиме перед backref нужно гарантировать наличие **2 байт** входных данных - -## Примеры данных - -### Пример 1: Литералы (простой режим) - -``` -Входные биты: 00 00 00 ... -Выход: Последовательность литералов - -Пример: - Flags: 0xFF (11111111) - Data: 0x41 ('A'), 0x42 ('B'), 0x43 ('C'), ... - Выход: "ABC..." -``` - -### Пример 2: LZ77 копирование - -``` -Входные биты: 10 ... -Выход: Копирование из окна - -Пример: - Flags: 0x00 (00000000) - первый бит = 0 - Bytes: b0=0x34, b1=0x12 - - Разбор: - offset = 0x34 | ((0x12 & 0xF0) << 4) = 0x234 - length = (0x12 & 0x0F) + 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}") - 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 deleted file mode 100644 index a65d595..0000000 --- a/docs/specs/assets/nres/huffman_decompression.md +++ /dev/null @@ -1,605 +0,0 @@ -# Huffman Декомпрессия - -## Обзор - -Это реализация **RAW-DEFLATE (inflate)**, используемого в [NRes](overview.md). Поток подаётся без zlib-обёртки (нет 2-байтового заголовка и Adler32). Алгоритм поддерживает три режима блоков и использует два Huffman дерева для кодирования литералов/длин и расстояний. - -```c -int __thiscall sub_1001AF10( - unsigned int *this, // Контекст декодера (HuffmanContext) - int *a2 // Выходной параметр (результат операции) -) -``` - -## Структура контекста (HuffmanContext) - -```c -struct HuffmanContext { - uint8_t window[0x10000]; // 0x00000-0x0FFFF: Внутренний буфер/окно - uint32_t compressedSize; // 0x10000: packedSize - uint32_t outputPosition; // 0x10004: Сколько уже выведено - uint32_t windowPos; // 0x10008: Позиция в 0x8000 окне - uint32_t sourcePtr; // 0x1000C: Указатель на сжатые данные - uint32_t destPtr; // 0x10010: Указатель на выходной буфер - uint32_t sourcePos; // 0x10014: Текущая позиция чтения - uint32_t unpackedSize; // 0x10018: Ожидаемый размер распаковки - uint32_t bitBufferValue; // 0x1001C: Битовый буфер - uint32_t bitsAvailable; // 0x10020: Количество доступных бит - uint32_t maxWindowPosSeen; // 0x10024: Максимум окна (статистика) - // ... -}; - -// Смещения в структуре (индексация this[]): -#define CTX_COMPRESSED_SIZE 0x4000 // this[0x4000] == 0x10000 -#define CTX_OUTPUT_POS 16385 // this[16385] == 0x10004 -#define CTX_WINDOW_POS 16386 // this[16386] == 0x10008 -#define CTX_SOURCE_PTR 16387 // this[16387] == 0x1000C -#define CTX_DEST_PTR 16388 // this[16388] == 0x10010 -#define CTX_SOURCE_POS 16389 // this[16389] == 0x10014 -#define CTX_UNPACKED_SIZE 16390 // this[16390] == 0x10018 -#define CTX_BIT_BUFFER 16391 // this[16391] == 0x1001C -#define CTX_BITS_COUNT 16392 // this[16392] == 0x10020 -#define CTX_MAX_WINDOW_POS 16393 // this[16393] == 0x10024 -``` - -## Три режима блоков - -Алгоритм определяет тип блока по первым 3 битам: - -``` -Биты: [TYPE:2] [FINAL:1] - -FINAL = 1: Это последний блок -TYPE: - 00 = Несжатый блок (сырые данные) - 01 = Сжатый с фиксированными Huffman кодами - 10 = Сжатый с динамическими Huffman кодами - 11 = Зарезервировано (ошибка) -``` - -Соответствие функциям: - -- type 0 → `sub_1001A750` (stored) -- type 1 → `sub_1001A8C0` (fixed Huffman) -- type 2 → `sub_1001AA30` (dynamic Huffman) - -### Основной цикл декодирования - -```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): - """Записать байт в выходной буфер""" - # Записываем в окно 0x8000 - ctx.window[ctx.windowPos] = byte - ctx.windowPos += 1 - - # Если окно заполнено (32KB) - if ctx.windowPos >= 0x8000: - flush_output_buffer(ctx) - - -def flush_output_buffer(ctx): - """Сбросить выходной буфер в финальный выход""" - # Копируем окно в финальный выходной буфер - dest_offset = ctx.outputPosition + ctx.destPtr - memcpy(dest_offset, ctx.window, ctx.windowPos) - - # Обновляем счетчики - ctx.outputPosition += ctx.windowPos - ctx.windowPos = 0 - - -def copy_from_history(ctx, distance, length): - """Скопировать данные из истории (LZ77)""" - # Позиция источника в циклическом буфере - src_pos = (ctx.windowPos - distance) & 0x7FFF - - for i in range(length): - byte = ctx.window[src_pos] - write_output_byte(ctx, byte) - src_pos = (src_pos + 1) & 0x7FFF -``` - -## Полная реализация на Python - -```python -class HuffmanDecoder: - """Полный RAW-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() -``` - -## Заключение - -Этот декодер реализует **RAW-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 deleted file mode 100644 index 60352cc..0000000 --- a/docs/specs/assets/nres/overview.md +++ /dev/null @@ -1,608 +0,0 @@ -# Документация по формату NRes - -## Обзор - -NRes — это формат контейнера ресурсов, используемый в игровом движке Nikita. Файл представляет собой архив, содержащий несколько упакованных файлов с метаданными и поддержкой различных методов сжатия. - -## Структура файла NRes - -### 1. Заголовок файла (16 байт) - -```c -struct NResHeader { - uint32_t signature; // +0x00: Сигнатура "NRes" (0x7365524E в little-endian) - uint32_t version; // +0x04: Версия формата (0x00000100 = версия 1.0) - uint32_t fileCount; // +0x08: Количество файлов в архиве - uint32_t fileSize; // +0x0C: Общий размер файла в байтах -}; -``` - -**Детали:** - -- `signature`: Константа `0x7365524E` (1936020046 в десятичном виде). Это ASCII строка "NRes" в обратном порядке байт -- `version`: Всегда должна быть `0x00000100` (256 в десятичном виде) для версии 1.0 -- `fileCount`: Общее количество файлов в архиве (используется для валидации) -- `fileSize`: Полный размер NRes файла, включая заголовок - -### 2. Данные файлов - -Сразу после заголовка (с offset 0x10) начинаются данные упакованных файлов. Они хранятся последовательно, один за другим. Точное расположение каждого файла определяется записью в каталоге (см. раздел 3). - -**⚠️ ВАЖНО: Выравнивание данных** - -Данные каждого файла **выравниваются по границе 8 байт**. После записи данных файла добавляется padding (нулевые байты) до ближайшего кратного 8 адреса. - -**Формула выравнивания:** - -``` -aligned_size = (packed_size + 7) & ~7 -padding_bytes = aligned_size - packed_size -``` - -**Пример:** - -- Файл размером 100 байт → padding 4 байта (до 104) -- Файл размером 104 байт → padding 0 байт (уже выровнен) -- Файл размером 105 байт → padding 3 байта (до 108) - -Это означает, что: - -1. `dataOffset` следующего файла всегда кратен 8 -2. Между данными файлов могут быть 0-7 байт нулевого padding -3. При чтении нужно использовать `packedSize`, а не выравнивать вручную - -### 3. Каталог файлов (Directory) - -Каталог находится в **конце файла**. Его расположение вычисляется по формуле: - -``` -DirectoryOffset = FileSize - (FileCount * 64) -``` - -Каждая запись в каталоге имеет **фиксированный размер 64 байта (0x40)**: - -```c -struct NResFileEntry { - char name[16]; // +0x00: Имя файла (NULL-terminated, uppercase) - uint32_t crc32; // +0x10: CRC32 хеш упакованных данных - uint32_t packMethod; // +0x14: Флаги метода упаковки (также используется как XOR seed) - uint32_t unpackedSize; // +0x18: Размер файла после распаковки - uint32_t packedSize; // +0x1C: Размер упакованных данных - uint32_t dataOffset; // +0x20: Смещение данных от начала файла - uint32_t fastDataPtr; // +0x24: Указатель для быстрого доступа (в памяти) - uint32_t xorSize; // +0x28: Размер данных для XOR-шифрования - uint32_t sortIndex; // +0x2C: Индекс для сортировки по имени - uint32_t reserved[4]; // +0x30: Зарезервировано (обычно нули) -}; -``` - -## Подробное описание полей каталога - -### Поле: name (смещение +0x00, 16 байт) - -- **Назначение**: Имя файла в архиве -- **Формат**: NULL-terminated строка, максимум 15 символов + NULL -- **Особенности**: - - Все символы хранятся в **UPPERCASE** (заглавными буквами) - - При поиске файлов используется регистронезависимое сравнение (`_strcmpi`) - - Если имя короче 16 байт, остаток заполняется нулями - -### Поле: crc32 (смещение +0x10, 4 байта) - -- **Назначение**: Контрольная сумма CRC32 упакованных данных -- **Использование**: Проверка целостности данных при чтении - -### Поле: packMethod (смещение +0x14, 4 байта) - -**Критически важное поле!** Содержит битовые флаги, определяющие метод обработки данных: - -```c -// Маски для извлечения метода упаковки -#define PACK_METHOD_MASK 0x1E0 // Биты 5-8 (метод + XOR) -#define PACK_METHOD_MASK2 0x1C0 // Биты 6-7 (без XOR-бита) - -// Методы упаковки (packMethod & 0x1E0) -#define PACK_NONE 0x000 // Нет упаковки (raw) -#define PACK_XOR 0x020 // XOR (только шифрование) -#define PACK_FRES 0x040 // FRES (LZSS простой режим) -#define PACK_FRES_XOR 0x060 // XOR + FRES -#define PACK_LZHUF 0x080 // LZHUF (LZSS + adaptive Huffman) -#define PACK_LZHUF_XOR 0x0A0 // XOR + LZHUF -#define PACK_DEFLATE_RAW 0x100 // RAW-DEFLATE (без zlib-обёртки) -``` - -**Алгоритм определения метода:** - -1. Извлечь биты `packMethod & 0x1E0` -2. Проверить конкретные значения: - - `0x000`: Данные не сжаты, простое копирование - - `0x020`: XOR-шифрование с двухбайтовым ключом - - `0x040` или `0x060`: FRES (может быть + XOR) - - `0x080` или `0x0A0`: LZHUF (может быть + XOR) - - `0x100`: RAW-DEFLATE (inflate без zlib-обёртки) - -**Важно:** `rsGetPackMethod()` возвращает `packMethod & 0x1C0`, то есть маску **без XOR-бита `0x20`**. Это нужно учитывать при сравнении. - -**Примечание про XOR seed:** значение для XOR берётся из поля `packMethod` (смещение `+0x14`). Это же поле может быть перезаписано при формировании каталога (см. раздел о `rsOpenLib`), если в библиотеке нет готовой таблицы сортировки. - -### Поле: unpackedSize (смещение +0x18, 4 байта) - -- **Назначение**: Размер файла после полной распаковки -- **Использование**: - - Для выделения памяти под распакованные данные - - Для проверки корректности распаковки - -### Поле: packedSize (смещение +0x1C, 4 байта) - -- **Назначение**: Размер сжатых данных в архиве -- **Особенности**: - - Если `packedSize == 0`, файл пустой или является указателем - - Для несжатых файлов: `packedSize == unpackedSize` - -### Поле: dataOffset (смещение +0x20, 4 байта) - -- **Назначение**: Абсолютное смещение данных файла от начала NRes файла -- **Формула вычисления**: `BaseAddress + dataOffset = начало данных` -- **Диапазон**: Обычно от 0x10 (после заголовка) до начала каталога - -### Поле: fastDataPtr (смещение +0x24, 4 байта) - -- **Назначение**: Указатель на данные в памяти для быстрого доступа -- **Использование**: Только во время выполнения (runtime) -- **В файле**: Обычно равно 0 или содержит относительный offset -- **Особенность**: Используется функцией `rsLoadFast()` для файлов без упаковки - -### Поле: xorSize (смещение +0x28, 4 байта) - -- **Назначение**: Размер данных для XOR-шифрования при комбинированных методах -- **Использование**: - - Когда `packMethod & 0x60 == 0x60` (FRES + XOR) - - Сначала применяется XOR к этому количеству байт, затем FRES к результату -- **Значение**: Может отличаться от `packedSize` при многоэтапной упаковке - -### Поле: sortIndex (смещение +0x2C, 4 байта) - -- **Назначение**: Индекс для быстрого поиска по отсортированному каталогу -- **Использование**: - - В `rsOpenLib` при отсутствии маркера `0xABBA` формируется таблица индексов сортировки имён - - Индексы записываются в это поле с шагом 0x40 (по записи) - - Используется `rsFind()` через таблицу индексов, а не прямую сортировку записей - -### Поле: reserved (смещение +0x30, 16 байт) - -- **Назначение**: Зарезервировано для будущих расширений -- **В файле**: Обычно заполнено нулями -- **Может содержать**: Дополнительные метаданные в новых версиях формата - -## Алгоритмы упаковки - -### 1. Без упаковки (PACK_NONE = 0x000) - -``` -Простое копирование данных: - memcpy(destination, source, packedSize); -``` - -### 2. XOR-шифрование (PACK_XOR = 0x020) - -```c -// Ключ/seed берется из поля packMethod (смещение +0x14) -uint16_t key = (uint16_t)(packMethod & 0xFFFF); - -for (int i = 0; i < packedSize; i++) { - uint8_t byte = source[i]; - destination[i] = byte ^ (key >> 8) ^ (key << 1); - - // Обновление ключа - uint8_t newByte = (key >> 8) ^ (key << 1); - key = (newByte ^ ((key >> 8) >> 1)) | (newByte << 8); -} -``` - -**Ключевые особенности:** - -- Используется 16-битный ключ из младших байт поля `packMethod` -- Ключ изменяется после каждого байта по специальному алгоритму -- Операции: XOR с старшим байтом ключа и со сдвинутым значением - -### 3. [FRES компрессия](fres_decompression.md) (PACK_FRES = 0x040, 0x060) - -Алгоритм FRES — это RLE-подобное сжатие с особой кодировкой повторов: - -``` -sub_1001B22E() - функция декомпрессии FRES - - Читает управляющие байты - - Декодирует литералы и повторы - - Использует скользящее окно для ссылок -``` - -### 4. [LZHUF (adaptive Huffman)](fres_decompression.md) (PACK_LZHUF = 0x080, 0x0A0) - -Наиболее сложный и эффективный метод: - -**Процесс декодирования:** - -1. Распаковка LZSS + adaptive Huffman (Okumura LZHUF) -2. Дерево обновляется после каждого символа -3. Match-символы преобразуются в длину и позицию - -### 5. [RAW-DEFLATE](huffman_decompression.md) (PACK_DEFLATE_RAW = 0x100) - -Это inflate без zlib-обёртки (без 2-байтового заголовка и Adler32). - -## Высокоуровневая инструкция по реализации - -### Этап 1: Открытие файла - -```python -def open_nres_file(filepath): - with open(filepath, 'rb') as f: - # 1. Читаем заголовок (16 байт) - header_data = f.read(16) - signature, version, file_count, file_size = struct.unpack('<4I', header_data) - - # 2. Проверяем сигнатуру - if signature != 0x7365524E: # "NRes" - raise ValueError("Неверная сигнатура файла") - - # 3. Проверяем версию - if version != 0x100: - raise ValueError(f"Неподдерживаемая версия: {version}") - - # 4. Вычисляем расположение каталога - directory_offset = file_size - (file_count * 64) - - # 5. Читаем весь файл в память (или используем memory mapping) - f.seek(0) - file_data = f.read() - - return { - 'file_count': file_count, - 'file_size': file_size, - 'directory_offset': directory_offset, - 'data': file_data - } -``` - -### Этап 2: Чтение каталога - -```python -def read_directory(nres_file): - data = nres_file['data'] - offset = nres_file['directory_offset'] - file_count = nres_file['file_count'] - - entries = [] - - for i in range(file_count): - entry_offset = offset + (i * 64) - entry_data = data[entry_offset:entry_offset + 64] - - # Парсим 64-байтовую запись - name = entry_data[0:16].decode('ascii').rstrip('\x00') - crc32, pack_method, unpacked_size, packed_size, data_offset, \ - fast_ptr, xor_size, sort_index = struct.unpack('<8I', entry_data[16:48]) - - entry = { - 'name': name, - 'crc32': crc32, - 'pack_method': pack_method, - 'unpacked_size': unpacked_size, - 'packed_size': packed_size, - 'data_offset': data_offset, - 'fast_data_ptr': fast_ptr, - 'xor_size': xor_size, - 'sort_index': sort_index - } - - entries.append(entry) - - return entries -``` - -### Этап 3: Поиск файла по имени - -```python -def find_file(entries, filename): - # Имена в архиве хранятся в UPPERCASE - search_name = filename.upper()[:15] - - # Используем бинарный поиск, так как каталог отсортирован - # Сортировка по sort_index восстанавливает алфавитный порядок - sorted_entries = sorted(entries, key=lambda e: e['sort_index']) - - left, right = 0, len(sorted_entries) - 1 - - while left <= right: - mid = (left + right) // 2 - mid_name = sorted_entries[mid]['name'] - - if mid_name == search_name: - return sorted_entries[mid] - elif mid_name < search_name: - left = mid + 1 - else: - right = mid - 1 - - return None -``` - -### Этап 4: Извлечение данных файла - -```python -def extract_file(nres_file, entry): - data = nres_file['data'] - - # 1. Получаем упакованные данные - packed_data = data[entry['data_offset']: - entry['data_offset'] + entry['packed_size']] - - # 2. Определяем метод упаковки - pack_method = entry['pack_method'] & 0x1E0 - - # 3. Распаковываем в зависимости от метода - if pack_method == 0x000: - # Без упаковки - return unpack_none(packed_data) - - elif pack_method == 0x020: - # XOR-шифрование - return unpack_xor(packed_data, entry['pack_method'], entry['unpacked_size']) - - elif pack_method == 0x040 or pack_method == 0x060: - # FRES компрессия (может быть с XOR) - if pack_method == 0x060: - # Сначала XOR - temp_data = unpack_xor(packed_data, entry['pack_method'], entry['xor_size']) - return unpack_fres(temp_data, entry['unpacked_size']) - else: - return unpack_fres(packed_data, entry['unpacked_size']) - - elif pack_method == 0x080 or pack_method == 0x0A0: - # LZHUF (может быть с XOR) - if pack_method == 0x0A0: - temp_data = unpack_xor(packed_data, entry['pack_method'], entry['xor_size']) - return unpack_lzhuf(temp_data, entry['unpacked_size']) - return unpack_lzhuf(packed_data, entry['unpacked_size']) - - elif pack_method == 0x100: - # RAW-DEFLATE - return unpack_deflate_raw(packed_data, entry['unpacked_size']) - - else: - raise ValueError(f"Неподдерживаемый метод упаковки: 0x{pack_method:X}") -``` - -### Этап 5: Реализация алгоритмов распаковки - -```python -def unpack_none(data): - """Без упаковки - просто возвращаем данные""" - return data - -def unpack_xor(data, pack_method, size): - """XOR-дешифрование с изменяющимся ключом""" - result = bytearray(size) - key = pack_method & 0xFFFF # Берем младшие 16 бит из поля packMethod - - for i in range(min(size, len(data))): - byte = data[i] - - # XOR операция - high_byte = (key >> 8) & 0xFF - shifted = (key << 1) & 0xFFFF - result[i] = byte ^ high_byte ^ (shifted & 0xFF) - - # Обновление ключа - new_byte = high_byte ^ (key << 1) - key = (new_byte ^ (high_byte >> 1)) | ((new_byte & 0xFF) << 8) - key &= 0xFFFF - - return bytes(result) - -def unpack_fres(data, unpacked_size): - """ - FRES декомпрессия - гибридный RLE+LZ77 алгоритм - Полная реализация в nres_decompression.py (класс FRESDecoder) - """ - from nres_decompression import FRESDecoder - decoder = FRESDecoder() - return decoder.decompress(data, unpacked_size) - -def unpack_lzhuf(data, unpacked_size): - """ - LZHUF (LZSS + adaptive Huffman) - Полная реализация в nres_decompression.py (класс LZHUDecoder) - """ - from nres_decompression import LZHUDecoder - decoder = LZHUDecoder() - return decoder.decompress(data, unpacked_size) - -def unpack_deflate_raw(data, unpacked_size): - """ - RAW-DEFLATE (inflate без zlib-обертки) - Полная реализация в nres_decompression.py (класс RawDeflateDecoder) - """ - from nres_decompression import RawDeflateDecoder - decoder = RawDeflateDecoder() - return decoder.decompress(data, unpacked_size) -``` - -### Этап 6: Извлечение всех файлов - -```python -def extract_all(nres_filepath, output_dir): - import os - - # 1. Открываем NRes файл - nres_file = open_nres_file(nres_filepath) - - # 2. Читаем каталог - entries = read_directory(nres_file) - - # 3. Создаем выходную директорию - os.makedirs(output_dir, exist_ok=True) - - # 4. Извлекаем каждый файл - for entry in entries: - print(f"Извлечение: {entry['name']}") - - try: - # Извлекаем данные - unpacked_data = extract_file(nres_file, entry) - - # Сохраняем в файл - output_path = os.path.join(output_dir, entry['name']) - with open(output_path, 'wb') as f: - f.write(unpacked_data) - - print(f" ✓ Успешно ({len(unpacked_data)} байт)") - - except Exception as e: - print(f" ✗ Ошибка: {e}") -``` - -## Поддерживаемые контейнеры - -### 1. NRes (MAGIC "NRes") - -- Открывается через `niOpenResFile/niOpenResInMem` -- Каталог находится в конце файла (см. структуру выше) - -### 2. rsLib / NL (MAGIC "NL") - -Отдельный формат контейнера, обрабатывается `rsOpenLib`: - -- В начале файла проверяется `*(_WORD*)buf == 0x4C4E` (ASCII "NL" в little-endian) -- `buf[3] == 1` — версия/маркер -- `buf[2]` — количество записей -- Каталог расположен с offset `0x20`, размер `0x20 * count` -- Каталог перед разбором дешифруется (байтовый XOR-поток) - -## Поиск по имени (rsFind) - -- Имя обрезается до 16 байт, `name[15] = 0` -- Приводится к верхнему регистру (`_strupr`) -- Поиск идёт по таблице индексов сортировки (значение хранится в поле `sortIndex`) -- Если в rsLib нет маркера `0xABBA`, таблица строится пузырьковой сортировкой и индексы записываются в поле записи - -## Особенности и важные замечания - -### 1. Порядок байт (Endianness) - -- **Все многобайтовые значения хранятся в Little-Endian порядке** -- При чтении используйте `struct.unpack('<...')` - -### 2. Сортировка каталога - -- Каталог файлов **отсортирован по имени файла** (алфавитный порядок) -- Поле `sortIndex` хранит оригинальный индекс до сортировки -- Это позволяет использовать бинарный поиск - -### 3. Регистр символов - -- Все имена файлов конвертируются в **UPPERCASE** (заглавные буквы) -- При поиске используйте регистронезависимое сравнение - -### 4. Memory Mapping - -- Оригинальный код использует `MapViewOfFile` для эффективной работы с большими файлами -- Рекомендуется использовать memory-mapped файлы для больших архивов - -### 5. Валидация данных - -- **Всегда проверяйте сигнатуру** перед обработкой -- **Проверяйте версию** формата -- **Проверяйте CRC32** после распаковки -- **Проверяйте размеры** (unpacked_size должен совпадать с результатом) - -### 6. Обработка ошибок - -- Файл может быть поврежден -- Метод упаковки может быть неподдерживаемым -- Данные могут быть частично зашифрованы - -### 7. Производительность - -- Для несжатых файлов (`packMethod & 0x1E0 == 0`) можно использовать прямое чтение -- Поле `fastDataPtr` может содержать кешированный указатель -- Используйте буферизацию при последовательном чтении - -### 8. Выравнивание данных - -- **Все данные файлов выравниваются по 8 байт** -- После каждого файла может быть 0-7 байт нулевого padding -- `dataOffset` следующего файла всегда кратен 8 -- При чтении используйте `packedSize` из записи, не вычисляйте выравнивание -- При создании архива добавляйте padding: `padding = ((size + 7) & ~7) - size` - -## Пример использования - -```python -# Открыть архив -nres = open_nres_file("resources.nres") - -# Прочитать каталог -entries = read_directory(nres) - -# Вывести список файлов -for entry in entries: - print(f"{entry['name']:20s} - {entry['unpacked_size']:8d} байт") - -# Найти конкретный файл -entry = find_file(entries, "texture.bmp") -if entry: - data = extract_file(nres, entry) - with open("extracted_texture.bmp", "wb") as f: - f.write(data) - -# Извлечь все файлы -extract_all("resources.nres", "./extracted/") -``` - -## Дополнительные функции - -### Проверка формата файла - -```python -def is_nres_file(filepath): - try: - with open(filepath, 'rb') as f: - signature = struct.unpack(' 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 представляет собой эффективный архив с поддержкой множества методов сжатия. -- cgit v1.2.3