diff options
Diffstat (limited to 'docs/specs')
| -rw-r--r-- | docs/specs/assets/nres/fres_decompression.md | 158 | ||||
| -rw-r--r-- | docs/specs/assets/nres/huffman_decompression.md | 79 | ||||
| -rw-r--r-- | docs/specs/assets/nres/overview.md | 156 |
3 files changed, 203 insertions, 190 deletions
diff --git a/docs/specs/assets/nres/fres_decompression.md b/docs/specs/assets/nres/fres_decompression.md index 1e0b894..6c73f66 100644 --- a/docs/specs/assets/nres/fres_decompression.md +++ b/docs/specs/assets/nres/fres_decompression.md @@ -51,37 +51,34 @@ dword_1003E0A4 // Длина повтора для LZ77 Цикл декомпрессии: Пока есть входные данные и выходной буфер не заполнен: - 1. Прочитать бит флага: - if (flagBits высокий бит == 0): + 1. Прочитать бит флага (LSB-first): + if (flagBits == 0): flags = *input++ - flagBits = 127 (0x7F) + flagBits = 8 flag_bit = flags & 1 flags >>= 1 + flagBits -= 1 - 2. Прочитать второй бит: - if (flagBits низкий бит == 0): - загрузить новый байт флагов + 2. Выбор действия по биту: - second_bit = flags & 1 - flags >>= 1 - - 3. Выбор действия по битам: - - a) Если оба бита == 0: + a) Если bit == 1: // Литерал - копировать один байт byte = *input++ window[position] = byte *output++ = byte position = (position + 1) & 0xFFF - b) Если второй бит == 0 (первый == 1): - // LZ77 копирование + b) Если bit == 0: + // LZ77 копирование (2 байта) word = *(uint16*)input input += 2 - offset = (word >> 4) & 0xFFF // 12 бит offset - length = (word & 0xF) + 3 // 4 бита длины + 3 + b0 = word & 0xFF + b1 = (word >> 8) & 0xFF + + offset = b0 | ((b1 & 0xF0) << 4) // 12 бит offset + length = (b1 & 0x0F) + 3 // 4 бита длины + 3 src_pos = offset Повторить length раз: @@ -97,19 +94,20 @@ dword_1003E0A4 // Длина повтора для LZ77 ``` Битовый поток: -[FLAG_BIT] [SECOND_BIT] [DATA] +Битовый поток: + +[FLAG_BIT] [DATA] Где: - FLAG_BIT = 0, SECOND_BIT = 0: → Литерал (1 байт следует) - FLAG_BIT = 1, SECOND_BIT = 0: → LZ77 копирование (2 байта следуют) - FLAG_BIT = любой, SECOND_BIT = 1: → Литерал (1 байт следует) + 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 = (byte1 >> 4) | (byte0 << 4) // 12 бит - length = (byte1 & 0x0F) + 3 // 4 бита + 3 = 3-18 байт + offset = byte0 | ((byte1 & 0xF0) << 4) // 12 бит + length = (byte1 & 0x0F) + 3 // 4 бита + 3 = 3-18 байт ``` ## Режим 2: Adaptive Huffman режим (a1 < 0) @@ -122,7 +120,8 @@ dword_1003E0A4 // Длина повтора для LZ77 Инициализация таблиц: 1. Создание таблицы быстрого декодирования (dword_1003B94C[256]) 2. Инициализация длин кодов (byte_1003BD4C[256]) - 3. Построение начального дерева (627 узлов) + 3. Построение начального дерева (627 узлов, T = 2*N_CHAR - 1) + где N_CHAR = 314 (256 литералов + 58 кодов длины) ``` ### Алгоритм декодирования @@ -161,14 +160,11 @@ dword_1003E0A4 // Длина повтора для LZ77 position = (position + 1) & 0xFFF else: - // LZ77 копирование - length = symbol - 253 - - // Прочитать offset (закодирован отдельно) - offset_bits = прочитать_биты(таблица длин) - offset = декодировать(offset_bits) + // LZSS копирование (LZHUF) + length = symbol - 253 // 3..60 + match_pos = decode_position() // префикс + 6 бит - src_pos = (position - 1 - offset) & 0xFFF + src_pos = (position - 1 - match_pos) & 0xFFF Повторить length раз: byte = window[src_pos] @@ -207,69 +203,23 @@ def fres_decompress_simple(input_data, output_size): input_pos = 0 flags = 0 - flag_bits_high = 0 - flag_bits_low = 0 + flag_bits = 0 while len(output) < output_size and input_pos < len(input_data): - # Читаем флаг высокого бита - if (flag_bits_high & 1) == 0: + # Читаем флаг (LSB-first) + if flag_bits == 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 + flag_bits = 8 - # Читаем флаг низкого бита - 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 + flag = flags & 1 flags >>= 1 + flag_bits -= 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 flag: # 1 = literal # Литерал if input_pos >= len(input_data): break @@ -279,6 +229,27 @@ def fres_decompress_simple(input_data, output_size): 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]) ``` @@ -347,7 +318,10 @@ def initialize_window(): ### 4. Битовые флаги -Используется двойная система флагов для определения типа следующих данных +Используется один флаговый бит (LSB-first) для определения типа данных: + +- `1` → literal (1 байт) +- `0` → backref (2 байта) ## Проблемы реализации @@ -363,6 +337,8 @@ def initialize_window(): Необходимо тщательно проверять границы буферов +- В простом режиме перед backref нужно гарантировать наличие **2 байт** входных данных + ## Примеры данных ### Пример 1: Литералы (простой режим) @@ -372,7 +348,7 @@ def initialize_window(): Выход: Последовательность литералов Пример: - Flags: 0x00 (00000000) + Flags: 0xFF (11111111) Data: 0x41 ('A'), 0x42 ('B'), 0x43 ('C'), ... Выход: "ABC..." ``` @@ -384,12 +360,12 @@ def initialize_window(): Выход: Копирование из окна Пример: - Flags: 0x01 (00000001) - первый бит = 1 - Word: 0x1234 + Flags: 0x00 (00000000) - первый бит = 0 + Bytes: b0=0x34, b1=0x12 Разбор: - offset = (0x34 << 4) | (0x12 >> 4) = 0x341 - length = (0x12 & 0xF) + 3 = 5 + offset = 0x34 | ((0x12 & 0xF0) << 4) = 0x234 + length = (0x12 & 0x0F) + 3 = 5 Действие: Скопировать 5 байт с позиции offset ``` @@ -406,7 +382,7 @@ def debug_fres_decompress(input_data, output_size): # ... реализация с print на каждом шаге - print(f"Flag: {flag_high}{flag_low}") + print(f"Flag: {flag}") if is_literal: print(f" Literal: 0x{byte:02X}") else: diff --git a/docs/specs/assets/nres/huffman_decompression.md b/docs/specs/assets/nres/huffman_decompression.md index aafc02b..a65d595 100644 --- a/docs/specs/assets/nres/huffman_decompression.md +++ b/docs/specs/assets/nres/huffman_decompression.md @@ -2,7 +2,7 @@ ## Обзор -Это реализация **DEFLATE-подобного** алгоритма декомпрессии, используемого в [NRes](overview.md). Алгоритм поддерживает три режима блоков и использует два Huffman дерева для кодирования литералов/длин и расстояний. +Это реализация **RAW-DEFLATE (inflate)**, используемого в [NRes](overview.md). Поток подаётся без zlib-обёртки (нет 2-байтового заголовка и Adler32). Алгоритм поддерживает три режима блоков и использует два Huffman дерева для кодирования литералов/длин и расстояний. ```c int __thiscall sub_1001AF10( @@ -15,30 +15,31 @@ int __thiscall sub_1001AF10( ```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]) + 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: Максимум окна (статистика) // ... }; -// Смещения в структуре: -#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] +// Смещения в структуре (индексация 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 ``` ## Три режима блоков @@ -56,6 +57,12 @@ TYPE: 11 = Зарезервировано (ошибка) ``` +Соответствие функциям: + +- type 0 → `sub_1001A750` (stored) +- type 1 → `sub_1001A8C0` (fixed Huffman) +- type 2 → `sub_1001AA30` (dynamic Huffman) + ### Основной цикл декодирования ```c @@ -417,33 +424,33 @@ def decode_huffman_symbol(ctx, tree): ```python def write_output_byte(ctx, byte): """Записать байт в выходной буфер""" - # Записываем в bitBuffer (используется как циклический буфер) - ctx.bitBuffer[ctx.decodedBytes] = byte - ctx.decodedBytes += 1 + # Записываем в окно 0x8000 + ctx.window[ctx.windowPos] = byte + ctx.windowPos += 1 - # Если буфер заполнен (32KB) - if ctx.decodedBytes >= 0x8000: + # Если окно заполнено (32KB) + if ctx.windowPos >= 0x8000: flush_output_buffer(ctx) def flush_output_buffer(ctx): """Сбросить выходной буфер в финальный выход""" - # Копируем данные в финальный выходной буфер - dest_offset = ctx.outputPosition + ctx.destData - memcpy(dest_offset, ctx.bitBuffer, ctx.decodedBytes) + # Копируем окно в финальный выходной буфер + dest_offset = ctx.outputPosition + ctx.destPtr + memcpy(dest_offset, ctx.window, ctx.windowPos) # Обновляем счетчики - ctx.outputPosition += ctx.decodedBytes - ctx.decodedBytes = 0 + ctx.outputPosition += ctx.windowPos + ctx.windowPos = 0 def copy_from_history(ctx, distance, length): """Скопировать данные из истории (LZ77)""" # Позиция источника в циклическом буфере - src_pos = (ctx.decodedBytes - distance) & 0x7FFF + src_pos = (ctx.windowPos - distance) & 0x7FFF for i in range(length): - byte = ctx.bitBuffer[src_pos] + byte = ctx.window[src_pos] write_output_byte(ctx, byte) src_pos = (src_pos + 1) & 0x7FFF ``` @@ -452,7 +459,7 @@ def copy_from_history(ctx, distance, length): ```python class HuffmanDecoder: - """Полный DEFLATE-подобный декодер""" + """Полный RAW-DEFLATE декодер""" def __init__(self, input_data, output_size): self.input_data = input_data @@ -582,7 +589,7 @@ def debug_huffman_decode(data): ## Заключение -Этот Huffman декодер реализует **DEFLATE**-совместимый алгоритм с тремя режимами блоков: +Этот декодер реализует **RAW-DEFLATE** с тремя режимами блоков: 1. **Несжатый** - для несжимаемых данных 2. **Фиксированный Huffman** - быстрое декодирование с предопределенными таблицами diff --git a/docs/specs/assets/nres/overview.md b/docs/specs/assets/nres/overview.md index f455862..60352cc 100644 --- a/docs/specs/assets/nres/overview.md +++ b/docs/specs/assets/nres/overview.md @@ -10,7 +10,7 @@ NRes — это формат контейнера ресурсов, исполь ```c struct NResHeader { - uint32_t signature; // +0x00: Сигнатура "NRes" (0x7365526E в little-endian) + uint32_t signature; // +0x00: Сигнатура "NRes" (0x7365524E в little-endian) uint32_t version; // +0x04: Версия формата (0x00000100 = версия 1.0) uint32_t fileCount; // +0x08: Количество файлов в архиве uint32_t fileSize; // +0x0C: Общий размер файла в байтах @@ -19,7 +19,7 @@ struct NResHeader { **Детали:** -- `signature`: Константа `0x7365526E` (1936020046 в десятичном виде). Это ASCII строка "nRes" в обратном порядке байт +- `signature`: Константа `0x7365524E` (1936020046 в десятичном виде). Это ASCII строка "NRes" в обратном порядке байт - `version`: Всегда должна быть `0x00000100` (256 в десятичном виде) для версии 1.0 - `fileCount`: Общее количество файлов в архиве (используется для валидации) - `fileSize`: Полный размер NRes файла, включая заголовок @@ -65,7 +65,7 @@ DirectoryOffset = FileSize - (FileCount * 64) struct NResFileEntry { char name[16]; // +0x00: Имя файла (NULL-terminated, uppercase) uint32_t crc32; // +0x10: CRC32 хеш упакованных данных - uint32_t packMethod; // +0x14: Флаги метода упаковки и опции + uint32_t packMethod; // +0x14: Флаги метода упаковки (также используется как XOR seed) uint32_t unpackedSize; // +0x18: Размер файла после распаковки uint32_t packedSize; // +0x1C: Размер упакованных данных uint32_t dataOffset; // +0x20: Смещение данных от начала файла @@ -98,20 +98,17 @@ struct NResFileEntry { ```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 // Файл зашифрован/требует декодирования +#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-обёртки) ``` **Алгоритм определения метода:** @@ -120,9 +117,13 @@ struct NResFileEntry { 2. Проверить конкретные значения: - `0x000`: Данные не сжаты, простое копирование - `0x020`: XOR-шифрование с двухбайтовым ключом - - `0x040` или `0x060`: FRES компрессия (может быть + XOR) - - `0x080` или `0x0A0`: Zlib компрессия (может быть + XOR) - - `0x0E0`: Huffman кодирование (наиболее распространенный) + - `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 байта) @@ -163,9 +164,9 @@ struct NResFileEntry { - **Назначение**: Индекс для быстрого поиска по отсортированному каталогу - **Использование**: - - Каталог сортируется по алфавиту (имени файлов) - - `sortIndex` хранит оригинальный порядковый номер файла - - Позволяет использовать бинарный поиск для функции `rsFind()` + - В `rsOpenLib` при отсутствии маркера `0xABBA` формируется таблица индексов сортировки имён + - Индексы записываются в это поле с шагом 0x40 (по записи) + - Используется `rsFind()` через таблицу индексов, а не прямую сортировку записей ### Поле: reserved (смещение +0x30, 16 байт) @@ -185,8 +186,8 @@ struct NResFileEntry { ### 2. XOR-шифрование (PACK_XOR = 0x020) ```c -// Ключ берется из поля crc32 -uint16_t key = (uint16_t)(crc32 & 0xFFFF); +// Ключ/seed берется из поля packMethod (смещение +0x14) +uint16_t key = (uint16_t)(packMethod & 0xFFFF); for (int i = 0; i < packedSize; i++) { uint8_t byte = source[i]; @@ -200,7 +201,7 @@ for (int i = 0; i < packedSize; i++) { **Ключевые особенности:** -- Используется 16-битный ключ из младших байт CRC32 +- Используется 16-битный ключ из младших байт поля `packMethod` - Ключ изменяется после каждого байта по специальному алгоритму - Операции: XOR с старшим байтом ключа и со сдвинутым значением @@ -215,30 +216,19 @@ sub_1001B22E() - функция декомпрессии FRES - Использует скользящее окно для ссылок ``` -### 4. [Huffman кодирование](huffman_decompression.md) (PACK_HUFFMAN = 0x0E0) +### 4. [LZHUF (adaptive Huffman)](fres_decompression.md) (PACK_LZHUF = 0x080, 0x0A0) Наиболее сложный и эффективный метод: -```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. Распаковка LZSS + adaptive Huffman (Okumura LZHUF) +2. Дерево обновляется после каждого символа +3. Match-символы преобразуются в длину и позицию + +### 5. [RAW-DEFLATE](huffman_decompression.md) (PACK_DEFLATE_RAW = 0x100) + +Это inflate без zlib-обёртки (без 2-байтового заголовка и Adler32). ## Высокоуровневая инструкция по реализации @@ -252,7 +242,7 @@ def open_nres_file(filepath): signature, version, file_count, file_size = struct.unpack('<4I', header_data) # 2. Проверяем сигнатуру - if signature != 0x7365526E: # "nRes" + if signature != 0x7365524E: # "NRes" raise ValueError("Неверная сигнатура файла") # 3. Проверяем версию @@ -315,7 +305,7 @@ def read_directory(nres_file): ```python def find_file(entries, filename): # Имена в архиве хранятся в UPPERCASE - search_name = filename.upper() + search_name = filename.upper()[:15] # Используем бинарный поиск, так как каталог отсортирован # Сортировка по sort_index восстанавливает алфавитный порядок @@ -357,20 +347,27 @@ def extract_file(nres_file, entry): elif pack_method == 0x020: # XOR-шифрование - return unpack_xor(packed_data, entry['crc32'], entry['unpacked_size']) + 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['crc32'], entry['xor_size']) + 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 == 0x0E0: - # Huffman кодирование - return unpack_huffman(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}") @@ -383,10 +380,10 @@ def unpack_none(data): """Без упаковки - просто возвращаем данные""" return data -def unpack_xor(data, crc32, size): +def unpack_xor(data, pack_method, size): """XOR-дешифрование с изменяющимся ключом""" result = bytearray(size) - key = crc32 & 0xFFFF # Берем младшие 16 бит + key = pack_method & 0xFFFF # Берем младшие 16 бит из поля packMethod for i in range(min(size, len(data))): byte = data[i] @@ -412,13 +409,22 @@ def unpack_fres(data, unpacked_size): decoder = FRESDecoder() return decoder.decompress(data, unpacked_size) -def unpack_huffman(data, unpacked_size): +def unpack_lzhuf(data, unpacked_size): """ - Huffman декодирование (DEFLATE-подобный) - Полная реализация в nres_decompression.py (класс HuffmanDecoder) + LZHUF (LZSS + adaptive Huffman) + Полная реализация в nres_decompression.py (класс LZHUDecoder) """ - from nres_decompression import HuffmanDecoder - decoder = HuffmanDecoder() + 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) ``` @@ -456,6 +462,30 @@ def extract_all(nres_filepath, output_dir): 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) @@ -539,7 +569,7 @@ def is_nres_file(filepath): try: with open(filepath, 'rb') as f: signature = struct.unpack('<I', f.read(4))[0] - return signature == 0x7365526E + return signature == 0x7365524E except: return False ``` @@ -553,9 +583,9 @@ def get_file_info(entry): 0x020: "XOR", 0x040: "FRES", 0x060: "FRES+XOR", - 0x080: "Zlib", - 0x0A0: "Zlib+XOR", - 0x0E0: "Huffman" + 0x080: "LZHUF", + 0x0A0: "LZHUF+XOR", + 0x100: "RAW-DEFLATE" } pack_method = entry['pack_method'] & 0x1E0 |
