diff options
| author | Valentin Popov <valentin@popov.link> | 2026-02-09 23:30:25 +0300 |
|---|---|---|
| committer | Valentin Popov <valentin@popov.link> | 2026-02-09 23:30:25 +0300 |
| commit | 54c94fddb5fcf4e38bc9124be0e9cec93a4cdcba (patch) | |
| tree | 3634e37f29360f5e02a4de1dd3513ec04be680d4 | |
| parent | 0def311fd17a0acfa2cc9bc70e0baf3c30a181c8 (diff) | |
| download | fparkan-54c94fddb5fcf4e38bc9124be0e9cec93a4cdcba.tar.xz fparkan-54c94fddb5fcf4e38bc9124be0e9cec93a4cdcba.zip | |
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.
| -rw-r--r-- | .gitignore | 20 | ||||
| -rw-r--r-- | docs/specs/assets/nres/fres_decompression.md | 402 | ||||
| -rw-r--r-- | docs/specs/assets/nres/huffman_decompression.md | 605 | ||||
| -rw-r--r-- | docs/specs/assets/nres/overview.md | 608 | ||||
| -rw-r--r-- | docs/specs/nres.md | 705 | ||||
| -rw-r--r-- | mkdocs.yml | 6 |
6 files changed, 720 insertions, 1626 deletions
@@ -64,9 +64,17 @@ $RECYCLE.BIN/ # Windows shortcuts *.lnk -# Zig programming language -zig-cache/ -zig-out/ -build/ -build-*/ -docgen_tmp/
\ No newline at end of file +# Generated by Cargo +# will have compiled files and executables +debug/ +target/ + +# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries +# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html +Cargo.lock + +# These are backup files generated by rustfmt +**/*.rs.bk + +# MSVC Windows builds of rustc generate these, which store debugging information +*.pdb
\ No newline at end of file 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('<I', f.read(4))[0] - return signature == 0x7365524E - except: - return False -``` - -### Получение информации о файле - -```python -def get_file_info(entry): - pack_names = { - 0x000: "Без сжатия", - 0x020: "XOR", - 0x040: "FRES", - 0x060: "FRES+XOR", - 0x080: "LZHUF", - 0x0A0: "LZHUF+XOR", - 0x100: "RAW-DEFLATE" - } - - 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 представляет собой эффективный архив с поддержкой множества методов сжатия. diff --git a/docs/specs/nres.md b/docs/specs/nres.md new file mode 100644 index 0000000..15cff63 --- /dev/null +++ b/docs/specs/nres.md @@ -0,0 +1,705 @@ +# Форматы игровых ресурсов + +## Обзор + +Библиотека `Ngi32.dll` реализует два различных формата архивов ресурсов: + +1. **NRes** — основной формат архива ресурсов, используемый через API `niOpenResFile` / `niCreateResFile`. Каталог файлов расположен в **конце** файла. Поддерживает создание, редактирование, добавление и удаление записей. + +2. **RsLi** — формат библиотеки ресурсов, используемый через API `rsOpenLib` / `rsLoad`. Таблица записей расположена **в начале** файла (сразу после заголовка) и зашифрована XOR-шифром. Поддерживает несколько методов сжатия. Только чтение. + +--- + +# Часть 1. Формат NRes + +## 1.1. Общая структура файла + +``` +┌──────────────────────────┐ Смещение 0 +│ Заголовок (16 байт) │ +├──────────────────────────┤ Смещение 16 +│ │ +│ Данные ресурсов │ +│ (выровнены по 8 байт) │ +│ │ +├──────────────────────────┤ Смещение = total_size - entry_count × 64 +│ Каталог записей │ +│ (entry_count × 64 байт) │ +└──────────────────────────┘ Смещение = total_size +``` + +## 1.2. Заголовок файла (16 байт) + +| Смещение | Размер | Тип | Значение | Описание | +| -------- | ------ | ------- | ------------------- | ------------------------------------ | +| 0 | 4 | char[4] | `NRes` (0x4E526573) | Магическая сигнатура (little-endian) | +| 4 | 4 | uint32 | `0x00000100` (256) | Версия формата (1.0) | +| 8 | 4 | int32 | — | Количество записей в каталоге | +| 12 | 4 | int32 | — | Полный размер файла в байтах | + +**Валидация при открытии:** магическая сигнатура и версия должны совпадать точно. Поле `total_size` (смещение 12) **проверяется на равенство** с фактическим размером файла (`GetFileSize`). Если значения не совпадают — файл отклоняется. + +## 1.3. Положение каталога в файле + +Каталог располагается в самом конце файла. Его смещение вычисляется по формуле: + +``` +directory_offset = total_size - entry_count × 64 +``` + +Данные ресурсов занимают пространство между заголовком (16 байт) и каталогом. + +## 1.4. Запись каталога (64 байта) + +Каждая запись каталога занимает ровно **64 байта** (0x40): + +| Смещение | Размер | Тип | Описание | +| -------- | ------ | -------- | ------------------------------------------------- | +| 0 | 4 | uint32 | Тип / идентификатор ресурса | +| 4 | 4 | uint32 | Атрибут 1 (например, формат, дата, категория) | +| 8 | 4 | uint32 | Атрибут 2 (например, подтип, метка времени) | +| 12 | 4 | uint32 | Размер данных ресурса в байтах | +| 16 | 4 | uint32 | Атрибут 3 (дополнительный параметр) | +| 20 | 36 | char[36] | Имя файла (null-terminated, макс. 35 символов) | +| 56 | 4 | uint32 | Смещение данных от начала файла | +| 60 | 4 | uint32 | Индекс сортировки (для двоичного поиска по имени) | + +### Поле «Имя файла» (смещение 20, 36 байт) + +- Максимальная длина имени: **35 символов** + 1 байт null-терминатор. +- При записи поле сначала обнуляется (`memset(0, 36 байт)`), затем копируется имя (`strncpy`, макс. 35 символов). +- Поиск по имени выполняется **без учёта регистра** (`_strcmpi`). + +### Поле «Индекс сортировки» (смещение 60) + +Используется для **двоичного поиска по имени**. Содержит индекс оригинальной записи, отсортированной в алфавитном порядке (регистронезависимо). Индекс строится при сохранении файла функцией `sub_10013260` с помощью **пузырьковой сортировки** по именам. + +**Алгоритм поиска** (`sub_10011E60`): классический двоичный поиск по отсортированному массиву индексов. Возвращает оригинальный индекс записи или `-1` при отсутствии. + +### Поле «Смещение данных» (смещение 56) + +Абсолютное смещение от начала файла. Данные читаются из mapped view: `pointer = mapped_base + data_offset`. + +## 1.5. Выравнивание данных + +При добавлении ресурса его данные записываются последовательно, после чего выполняется **выравнивание по 8-байтной границе**: + +```c +padding = ((data_size + 7) & ~7) - data_size; +// Если padding > 0, записываются нулевые байты +``` + +Таким образом, каждый блок данных начинается с адреса, кратного 8. + +При изменении размера данных ресурса выполняется сдвиг всех последующих данных и обновление смещений всех затронутых записей каталога. + +## 1.6. Создание файла (API `niCreateResFile`) + +При создании нового файла: + +1. Если файл уже существует и содержит корректный NRes-архив, существующий каталог считывается с конца файла, а файл усекается до начала каталога. +2. Если файл пуст или не является NRes-архивом, создаётся новый с пустым каталогом. Поля `entry_count = 0`, `total_size = 16`. + +При закрытии файла (`sub_100122D0`): + +1. Заголовок переписывается в начало файла (16 байт). +2. Вычисляется `total_size = data_end_offset + entry_count × 64`. +3. Индексы сортировки пересчитываются. +4. Каталог записей записывается в конец файла. + +## 1.7. Режимы сортировки каталога + +Функция `sub_10012560` поддерживает 12 режимов сортировки (0–11): + +| Режим | Порядок сортировки | +| ----- | --------------------------------- | +| 0 | Без сортировки (сброс) | +| 1 | По атрибуту 1 (смещение 4) | +| 2 | По атрибуту 2 (смещение 8) | +| 3 | По (атрибут 1, атрибут 2) | +| 4 | По типу ресурса (смещение 0) | +| 5 | По (тип, атрибут 1) | +| 6 | По (тип, атрибут 1) — идентичен 5 | +| 7 | По (тип, атрибут 1, атрибут 2) | +| 8 | По имени (регистронезависимо) | +| 9 | По (тип, имя) | +| 10 | По (атрибут 1, имя) | +| 11 | По (атрибут 2, имя) | + +## 1.8. Операция `niOpenResFileEx` — флаги открытия + +Второй параметр — битовые флаги: + +| Бит | Маска | Описание | +| --- | ----- | ----------------------------------------------------------------------------------- | +| 0 | 0x01 | Sequential scan hint (`FILE_FLAG_SEQUENTIAL_SCAN` вместо `FILE_FLAG_RANDOM_ACCESS`) | +| 1 | 0x02 | Открыть для записи (read-write). Без флага — только чтение | +| 2 | 0x04 | Пометить файл как «кэшируемый» (не выгружать при refcount=0) | +| 3 | 0x08 | Raw-режим: не проверять заголовок NRes, трактовать весь файл как единый ресурс | + +## 1.9. Виртуальное касание страниц + +Функция `sub_100197D0` выполняет «касание» страниц памяти для принудительной загрузки из memory-mapped файла. Она обходит адресное пространство с шагом 4096 байт (размер страницы), начиная с 0x10000 (64 КБ): + +``` +for (result = 0x10000; result < size; result += 4096); +``` + +Вызывается при чтении данных ресурса с флагом `a3 != 0` для предзагрузки данных в оперативную память. + +--- + +# Часть 2. Формат RsLi + +## 2.1. Общая структура файла + +``` +┌───────────────────────────────┐ Смещение 0 +│ Заголовок файла (32 байта) │ +├───────────────────────────────┤ Смещение 32 +│ Таблица записей (зашифрована)│ +│ (entry_count × 32 байт) │ +├───────────────────────────────┤ Смещение 32 + entry_count × 32 +│ │ +│ Данные ресурсов │ +│ │ +├───────────────────────────────┤ +│ [Опциональный трейлер — 6 б] │ +└───────────────────────────────┘ +``` + +## 2.2. Заголовок файла (32 байта) + +| Смещение | Размер | Тип | Значение | Описание | +| -------- | ------ | ------- | ----------------- | --------------------------------------------- | +| 0 | 2 | char[2] | `NL` (0x4C4E) | Магическая сигнатура | +| 2 | 1 | uint8 | `0x00` | Зарезервировано (должно быть 0) | +| 3 | 1 | uint8 | `0x01` | Версия формата | +| 4 | 2 | int16 | — | Количество записей (sign-extended при чтении) | +| 6 | 8 | — | — | Зарезервировано / не используется | +| 14 | 2 | uint16 | `0xABBA` или иное | Флаг предсортировки (см. ниже) | +| 16 | 4 | — | — | Зарезервировано | +| 20 | 4 | uint32 | — | **Начальное состояние XOR-шифра** (seed) | +| 24 | 8 | — | — | Зарезервировано | + +### Флаг предсортировки (смещение 14) + +- Если `*(uint16*)(header + 14) == 0xABBA` — движок **не строит** таблицу индексов в памяти. Значения `entry[i].sort_to_original` используются **как есть** (и для двоичного поиска, и как XOR‑ключ для данных). +- Если значение **отлично от 0xABBA** — после загрузки выполняется **пузырьковая сортировка** имён и строится перестановка `sort_to_original[]`, которая затем **записывается в `entry[i].sort_to_original`**, перетирая значения из файла. Именно эта перестановка далее используется и для поиска, и как XOR‑ключ (младшие 16 бит). + +## 2.3. XOR-шифр таблицы записей + +Таблица записей начинается со смещения 32 и зашифрована поточным XOR-шифром. Ключ инициализируется из DWORD по смещению 20 заголовка. + +### Начальное состояние + +``` +seed = *(uint32*)(header + 20) +lo = seed & 0xFF // Младший байт +hi = (seed >> 8) & 0xFF // Второй байт +``` + +### Алгоритм дешифровки (побайтовый) + +Для каждого зашифрованного байта `encrypted[i]`, начиная с `i = 0`: + +``` +step 1: lo = hi ^ ((lo << 1) & 0xFF) // Сдвиг lo влево на 1, XOR с hi +step 2: decrypted[i] = lo ^ encrypted[i] // Расшифровка байта +step 3: hi = lo ^ ((hi >> 1) & 0xFF) // Сдвиг hi вправо на 1, XOR с lo +``` + +**Пример реализации:** + +```python +def decrypt_rs_entries(encrypted_data: bytes, seed: int) -> bytes: + lo = seed & 0xFF + hi = (seed >> 8) & 0xFF + result = bytearray(len(encrypted_data)) + for i in range(len(encrypted_data)): + lo = (hi ^ ((lo << 1) & 0xFF)) & 0xFF + result[i] = lo ^ encrypted_data[i] + hi = (lo ^ ((hi >> 1) & 0xFF)) & 0xFF + return bytes(result) +``` + +Этот же алгоритм используется для шифрования данных ресурсов с методом XOR (флаги 0x20, 0x60, 0xA0), но с другим начальным ключом из записи. + +## 2.4. Запись таблицы (32 байта, на диске, до дешифровки) + +После дешифровки каждая запись имеет следующую структуру: + +| Смещение | Размер | Тип | Описание | +| -------- | ------ | -------- | -------------------------------------------------------------- | +| 0 | 12 | char[12] | Имя ресурса (ASCII, обычно uppercase; строка читается до `\0`) | +| 12 | 4 | — | Зарезервировано (движком игнорируется) | +| 16 | 2 | int16 | **Флаги** (метод сжатия и атрибуты) | +| 18 | 2 | int16 | **`sort_to_original[i]` / XOR‑ключ** (см. ниже) | +| 20 | 4 | uint32 | **Размер распакованных данных** (`unpacked_size`) | +| 24 | 4 | uint32 | Смещение данных от начала файла (`data_offset`) | +| 28 | 4 | uint32 | Размер упакованных данных в байтах (`packed_size`) | + +### Имена ресурсов + +- Поле `name[12]` копируется побайтно. Внутренне движок всегда имеет `\0` сразу после этих 12 байт (зарезервированные 4 байта в памяти принудительно обнуляются), поэтому имя **может быть длиной до 12 символов** даже без `\0` внутри `name[12]`. +- На практике имена обычно **uppercase ASCII**. `rsFind` приводит запрос к верхнему регистру (`_strupr`) и сравнивает побайтно. +- `rsFind` копирует имя запроса `strncpy(..., 16)` и принудительно ставит `\0` в `Destination[15]`, поэтому запрос длиннее 15 символов будет усечён. + +### Поле `sort_to_original[i]` (смещение 18) + +Это **не “свойство записи”**, а элемент таблицы индексов, по которой `rsFind` делает двоичный поиск: + +- Таблица реализована “внутри записей”: значение берётся как `entry[i].sort_to_original` (где `i` — позиция двоичного поиска), а реальная запись для сравнения берётся как `entry[ sort_to_original[i] ]`. +- Тем же значением (младшие 16 бит) инициализируется XOR‑шифр данных для методов, где он используется (0x20/0x60/0xA0). Поэтому при упаковке/шифровании данных ключ должен совпадать с итоговым `sort_to_original[i]` (см. флаг 0xABBA в разделе 2.2). + +Поиск выполняется **двоичным поиском** по этой таблице, с фолбэком на **линейный поиск** если двоичный не нашёл (поведение `rsFind`). + +## 2.5. Поле флагов (смещение 16 записи) + +Биты поля флагов кодируют метод сжатия и дополнительные атрибуты: + +``` +Биты [8:5] (маска 0x1E0): Метод сжатия/шифрования +Бит [6] (маска 0x040): Флаг realloc (буфер декомпрессии может быть больше) +``` + +### Методы сжатия (биты 8–5, маска 0x1E0) + +| Значение | Hex | Описание | +| -------- | ----- | --------------------------------------- | +| 0x000 | 0x00 | Без сжатия (копирование) | +| 0x020 | 0x20 | Только XOR-шифр | +| 0x040 | 0x40 | LZSS (простой вариант) | +| 0x060 | 0x60 | XOR-шифр + LZSS (простой вариант) | +| 0x080 | 0x80 | LZSS с адаптивным кодированием Хаффмана | +| 0x0A0 | 0xA0 | XOR-шифр + LZSS с Хаффманом | +| 0x100 | 0x100 | Deflate (аналог zlib/RFC 1951) | + +Примечание: `rsGetPackMethod()` возвращает `flags & 0x1C0` (без бита 0x20). Поэтому: + +- для 0x20 вернётся 0x00, +- для 0x60 вернётся 0x40, +- для 0xA0 вернётся 0x80. + +### Бит 0x40 (выделение +0x12 и последующее `realloc`) + +Бит 0x40 проверяется отдельно (`flags & 0x40`). Если он установлен, выходной буфер выделяется с запасом `+0x12` (18 байт), а после распаковки вызывается `realloc` для усечения до точного `unpacked_size`. + +Важно: этот же бит входит в код методов 0x40/0x60, поэтому для них поведение “+0x12 и shrink” включено автоматически. + +## 2.6. Размеры данных + +В каждой записи на диске хранятся оба значения: + +- `unpacked_size` (смещение 20) — размер распакованных данных. +- `packed_size` (смещение 28) — размер упакованных данных (байт во входном потоке для выбранного метода). + +Для метода 0x00 (без сжатия) обычно `packed_size == unpacked_size`. + +`rsGetInfo` возвращает именно `unpacked_size` (то, сколько байт выдаст `rsLoad`). + +## 2.7. Опциональный трейлер медиа (6 байт) + +При открытии с флагом `a2 & 2`: + +| Смещение от конца | Размер | Тип | Описание | +| ----------------- | ------ | ------- | ----------------------- | +| −6 | 2 | char[2] | Сигнатура `AO` (0x4F41) | +| −4 | 4 | uint32 | Смещение медиа-оверлея | + +Если трейлер присутствует, все смещения данных в записях корректируются: `effective_offset = entry_offset + media_overlay_offset`. + +--- + +# Часть 3. Алгоритмы сжатия (формат RsLi) + +## 3.1. XOR-шифр данных (метод 0x20) + +Алгоритм идентичен XOR‑шифру таблицы записей (раздел 2.3), но начальный ключ берётся из `entry[i].sort_to_original` (смещение 18 записи, младшие 16 бит). + +Важно про размер входа: + +- В ветке **0x20** движок XOR‑ит ровно `unpacked_size` байт (и ожидает, что поток данных имеет ту же длину; на практике `packed_size == unpacked_size`). +- В ветках **0x60/0xA0** XOR применяется к **упакованному** потоку длиной `packed_size` перед декомпрессией. + +### Инициализация + +``` +key16 = (uint16)entry.sort_to_original // int16 на диске по смещению 18 +lo = key16 & 0xFF +hi = (key16 >> 8) & 0xFF +``` + +### Дешифровка (псевдокод) + +``` +for i in range(N): # N = unpacked_size (для 0x20) или packed_size (для 0x60/0xA0) + lo = (hi ^ ((lo << 1) & 0xFF)) & 0xFF + out[i] = in[i] ^ lo + hi = (lo ^ ((hi >> 1) & 0xFF)) & 0xFF +``` + +## 3.2. LZSS — простой вариант (метод 0x40) + +Классический алгоритм LZSS (Lempel-Ziv-Storer-Szymanski) с кольцевым буфером. + +### Параметры + +| Параметр | Значение | +| ----------------------------- | ------------------ | +| Размер кольцевого буфера | 4096 байт (0x1000) | +| Начальная позиция записи | 4078 (0xFEE) | +| Начальное заполнение | 0x20 (пробел) | +| Минимальная длина совпадения | 3 | +| Максимальная длина совпадения | 18 (4 бита + 3) | + +### Алгоритм декомпрессии + +``` +Инициализация: + ring_buffer[0..4095] = 0x20 (заполнить пробелами) + ring_pos = 4078 + flags_byte = 0 + flags_bits_remaining = 0 + +Цикл (пока не заполнен выходной буфер И не исчерпан входной): + + 1. Если flags_bits_remaining == 0: + - Прочитать 1 байт из входного потока → flags_byte + - flags_bits_remaining = 8 + + Декодировать как: + - Старший бит устанавливается в 0x7F (маркер) + - Оставшиеся 7 бит — флаги текущей группы + + Реально в коде: control_word = (flags_byte) | (0x7F << 8) + Каждый бит проверяется сдвигом вправо. + + 2. Проверить младший бит control_word: + + Если бит = 1 (литерал): + - Прочитать 1 байт из входного потока → byte + - ring_buffer[ring_pos] = byte + - ring_pos = (ring_pos + 1) & 0xFFF + - Записать byte в выходной буфер + + Если бит = 0 (ссылка): + - Прочитать 2 байта: low_byte, high_byte + - offset = low_byte | ((high_byte & 0x0F) << 8) // 12 бит + - length = ((high_byte >> 4) & 0x0F) + 3 // 4 бита + 3 + - Скопировать length байт из ring_buffer[offset...]: + для j от 0 до length-1: + byte = ring_buffer[(offset + j) & 0xFFF] + ring_buffer[ring_pos] = byte + ring_pos = (ring_pos + 1) & 0xFFF + записать byte в выходной буфер + + 3. Сдвинуть control_word вправо на 1 бит + 4. flags_bits_remaining -= 1 +``` + +### Подробная раскладка пары ссылки (2 байта) + +``` +Байт 0 (low): OOOOOOOO (биты [7:0] смещения) +Байт 1 (high): LLLLOOOO L = длина − 3, O = биты [11:8] смещения + +offset = low | ((high & 0x0F) << 8) // Диапазон: 0–4095 +length = (high >> 4) + 3 // Диапазон: 3–18 +``` + +## 3.3. LZSS с адаптивным кодированием Хаффмана (метод 0x80) + +Расширенный вариант LZSS, где литералы и длины совпадений кодируются с помощью адаптивного дерева Хаффмана. + +### Параметры + +| Параметр | Значение | +| -------------------------------- | ------------------------------ | +| Размер кольцевого буфера | 4096 байт | +| Начальная позиция записи | **4036** (0xFC4) | +| Начальное заполнение | 0x20 (пробел) | +| Количество листовых узлов дерева | 314 | +| Символы литералов | 0–255 (байты) | +| Символы длин | 256–313 (длина = символ − 253) | +| Начальная длина | 3 (при символе 256) | +| Максимальная длина | 60 (при символе 313) | + +### Дерево Хаффмана + +Дерево строится как **адаптивное** (dynamic, self-adjusting): + +- **627 узлов**: 314 листовых + 313 внутренних. +- Все листья изначально имеют **вес 1**. +- Корень дерева — узел с индексом 0 (в массиве `parent`). +- После декодирования каждого символа дерево **обновляется** (функция `sub_1001B0AE`): вес узла инкрементируется, и при нарушении порядка узлы **переставляются** для поддержания свойства. +- При достижении суммарного веса **0x8000 (32768)** — все веса **делятся на 2** (с округлением вверх) и дерево полностью перестраивается. + +### Кодирование позиции + +Позиция в кольцевом буфере кодируется с помощью **d-кода** (таблица дистанций): + +- 8 бит позиции ищутся в таблице `d_code[256]`, определяя базовое значение и количество дополнительных битов. +- Из потока считываются дополнительные биты, которые объединяются с базовым значением. +- Финальная позиция: `pos = (ring_pos − 1 − decoded_position) & 0xFFF` + +**Таблицы инициализации** (d-коды): + +``` +Таблица базовых значений — byte_100371D0[6]: + { 0x01, 0x03, 0x08, 0x0C, 0x18, 0x10 } + +Таблица дополнительных битов — byte_100371D6[6]: + { 0x20, 0x30, 0x40, 0x30, 0x30, 0x10 } +``` + +### Алгоритм декомпрессии (высокоуровневый) + +``` +Инициализация: + ring_buffer[0..4095] = 0x20 + ring_pos = 4036 + Инициализировать дерево Хаффмана (314 листьев, все веса = 1) + Инициализировать таблицы d-кодов + +Цикл: + 1. Декодировать символ из потока по дереву Хаффмана: + - Начать с корня + - Читать биты, спускаться по дереву (0 = левый, 1 = правый) + - Пока не достигнут лист → символ = лист − 627 + + 2. Обновить дерево Хаффмана для декодированного символа + + 3. Если символ < 256 (литерал): + - ring_buffer[ring_pos] = символ + - ring_pos = (ring_pos + 1) & 0xFFF + - Записать символ в выходной буфер + + 4. Если символ >= 256 (ссылка): + - length = символ − 253 + - Декодировать позицию через d-код: + a) Прочитать 8 бит из потока + b) Найти d-код и дополнительные биты по таблице + c) Прочитать дополнительные биты + d) position = (ring_pos − 1 − full_position) & 0xFFF + - Скопировать length байт из ring_buffer[position...] + + 5. Если выходной буфер заполнен → завершить +``` + +## 3.4. XOR + LZSS (методы 0x60 и 0xA0) + +Комбинированный метод: сначала XOR-дешифровка, затем LZSS-декомпрессия. + +### Алгоритм + +1. Выделить временный буфер размером `compressed_size` (поле из записи, смещение 28). +2. Дешифровать сжатые данные XOR-шифром (раздел 3.1) с ключом из записи во временный буфер. +3. Применить LZSS-декомпрессию (простую или с Хаффманом, в зависимости от конкретного метода) из временного буфера в выходной. +4. Освободить временный буфер. + +- **0x60** — XOR + простой LZSS (раздел 3.2) +- **0xA0** — XOR + LZSS с Хаффманом (раздел 3.3) + +### Начальное состояние XOR для данных + +При комбинированном методе seed берётся из поля по смещению 20 записи (4-байтный). Однако ключ обрабатывается как 16-битный: `lo = seed & 0xFF`, `hi = (seed >> 8) & 0xFF`. + +## 3.5. Deflate (метод 0x100) + +Полноценная реализация алгоритма **Deflate** (RFC 1951) с блочной структурой. + +### Общая структура + +Данные состоят из последовательности блоков. Каждый блок начинается с: + +- **1 бит** — `is_final`: признак последнего блока +- **2 бита** — `block_type`: тип блока + +### Типы блоков + +| block_type | Описание | Функция | +| ---------- | --------------------------- | ---------------- | +| 0 | Без сжатия (stored) | `sub_1001A750` | +| 1 | Фиксированные коды Хаффмана | `sub_1001A8C0` | +| 2 | Динамические коды Хаффмана | `sub_1001AA30` | +| 3 | Зарезервировано (ошибка) | Возвращает код 2 | + +### Блок типа 0 (stored) + +1. Отбросить оставшиеся биты до границы байта (выравнивание). +2. Прочитать 16 бит — `LEN` (длина блока). +3. Прочитать 16 бит — `NLEN` (дополнение длины, `NLEN == ~LEN & 0xFFFF`). +4. Проверить: `LEN == (uint16)(~NLEN)`. При несовпадении — ошибка. +5. Скопировать `LEN` байт из входного потока в выходной. + +Декомпрессор использует внутренний буфер размером **32768 байт** (0x8000). При заполнении — промежуточная запись результата. + +### Блок типа 1 (фиксированные коды) + +Стандартные коды Deflate: + +- Литералы/длины: 288 кодов + - 0–143: 8-битные коды + - 144–255: 9-битные коды + - 256–279: 7-битные коды + - 280–287: 8-битные коды +- Дистанции: 30 кодов, все 5-битные + +Используются предопределённые таблицы длин и дистанций (`unk_100370AC`, `unk_1003712C` и соответствующие экстра-биты). + +### Блок типа 2 (динамические коды) + +1. Прочитать 5 бит → `HLIT` (количество литералов/длин − 257). Диапазон: 257–286. +2. Прочитать 5 бит → `HDIST` (количество дистанций − 1). Диапазон: 1–30. +3. Прочитать 4 бита → `HCLEN` (количество кодов длин − 4). Диапазон: 4–19. +4. Прочитать `HCLEN` × 3 бит — длины кодов для алфавита длин. +5. Построить дерево Хаффмана для алфавита длин (19 символов). +6. С помощью этого дерева декодировать длины кодов для литералов/длин и дистанций. +7. Построить два дерева Хаффмана: для литералов/длин и для дистанций. +8. Декодировать данные. + +**Порядок кодов длин** (стандартный Deflate): + +``` +{ 16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15 } +``` + +Хранится в `dword_10037060`. + +### Валидации + +- `HLIT + 257 <= 286` (max 0x11E) +- `HDIST + 1 <= 30` (max 0x1E) +- При нарушении — возвращается ошибка 1. + +## 3.6. Метод 0x00 (без сжатия) + +Данные копируются «как есть» напрямую из файла. Вызывается через указатель на функцию `dword_1003A1B8` (фактически `memcpy` или аналог). + +--- + +# Часть 4. Внутренние структуры в памяти + +## 4.1. Внутренняя структура NRes-архива (opened, 0x68 байт = 104) + +```c +struct NResArchive { // Размер: 0x68 (104 байта) + void* vtable; // +0: Указатель на таблицу виртуальных методов + int32_t entry_count; // +4: Количество записей + void* mapped_base; // +8: Базовый адрес mapped view + void* directory_ptr; // +12: Указатель на каталог записей в памяти + char* filename; // +16: Путь к файлу (_strdup) + int32_t ref_count; // +20: Счётчик ссылок + uint32_t last_release_time; // +24: timeGetTime() при последнем Release + // +28..+91: Для raw-режима — встроенная запись (единственный File entry) + NResArchive* next; // +92: Следующий архив в связном списке + uint8_t is_writable; // +100: Файл открыт для записи + uint8_t is_cacheable; // +101: Не выгружать при refcount = 0 +}; +``` + +## 4.2. Внутренняя структура RsLi-архива (56 + 64 × N байт) + +```c +struct RsLibHeader { // 56 байт (14 DWORD) + uint32_t magic; // +0: 'RsLi' (0x694C7352) + int32_t entry_count; // +4: Количество записей + uint32_t media_offset; // +8: Смещение медиа-оверлея + uint32_t reserved_0C; // +12: 0 + HANDLE file_handle_2; // +16: -1 (дополнительный хэндл) + uint32_t reserved_14; // +20: 0 + uint32_t reserved_18; // +24: — + uint32_t reserved_1C; // +28: 0 + HANDLE mapping_handle_2; // +32: -1 + uint32_t reserved_24; // +36: 0 + uint32_t flag_28; // +40: (flags >> 7) & 1 + HANDLE file_handle; // +44: Хэндл файла + HANDLE mapping_handle; // +48: Хэндл файлового маппинга + void* mapped_view; // +52: Указатель на mapped view +}; +// Далее следуют entry_count записей по 64 байта каждая +``` + +### Внутренняя запись RsLi (64 байта) + +```c +struct RsLibEntry { // 64 байта (16 DWORD) + char name[16]; // +0: Имя (12 из файла + 4 нуля) + int32_t flags; // +16: Флаги (sign-extended из int16) + int32_t sort_index; // +20: sort_to_original[i] (таблица индексов / XOR‑ключ) + uint32_t uncompressed_size; // +24: Размер несжатых данных (из поля 20 записи) + void* data_ptr; // +28: Указатель на данные в mapped view + uint32_t compressed_size; // +32: Размер сжатых данных (из поля 28 записи) + uint32_t reserved_24; // +36: 0 + uint32_t reserved_28; // +40: 0 + uint32_t reserved_2C; // +44: 0 + void* loaded_data; // +48: Указатель на декомпрессированные данные + // +52..+63: дополнительные поля +}; +``` + +--- + +# Часть 5. Экспортируемые API-функции + +## 5.1. NRes API + +| Функция | Описание | +| ------------------------------ | ------------------------------------------------------------------------- | +| `niOpenResFile(path)` | Открыть NRes-архив (только чтение), эквивалент `niOpenResFileEx(path, 0)` | +| `niOpenResFileEx(path, flags)` | Открыть NRes-архив с флагами | +| `niOpenResInMem(ptr, size)` | Открыть NRes-архив из памяти | +| `niCreateResFile(path)` | Создать/открыть NRes-архив для записи | + +## 5.2. RsLi API + +| Функция | Описание | +| ------------------------------- | -------------------------------------------------------- | +| `rsOpenLib(path, flags)` | Открыть RsLi-библиотеку | +| `rsCloseLib(lib)` | Закрыть библиотеку | +| `rsLibNum(lib)` | Получить количество записей | +| `rsFind(lib, name)` | Найти запись по имени (→ индекс или −1) | +| `rsLoad(lib, index)` | Загрузить и декомпрессировать ресурс | +| `rsLoadFast(lib, index, flags)` | Быстрая загрузка (без декомпрессии если возможно) | +| `rsLoadPacked(lib, index)` | Загрузить в «упакованном» виде (отложенная декомпрессия) | +| `rsLoadByName(lib, name)` | `rsFind` + `rsLoad` | +| `rsGetInfo(lib, index, out)` | Получить имя и размер ресурса | +| `rsGetPackMethod(lib, index)` | Получить метод сжатия (`flags & 0x1C0`) | +| `ngiUnpack(packed)` | Декомпрессировать ранее загруженный упакованный ресурс | +| `ngiAlloc(size)` | Выделить память (с обработкой ошибок) | +| `ngiFree(ptr)` | Освободить память | +| `ngiGetMemSize(ptr)` | Получить размер выделенного блока | + +--- + +# Часть 6. Контрольные заметки для реализации + +## 6.1. Кодировки и регистр + +- **NRes**: имена хранятся **как есть** (case-insensitive при поиске через `_strcmpi`). +- **RsLi**: имена хранятся в **верхнем регистре**. Перед поиском запрос приводится к верхнему регистру (`_strupr`). Сравнение — через `strcmp` (case-sensitive для уже uppercase строк). + +## 6.2. Порядок байт + +Все значения хранятся в **little-endian** порядке (платформа x86/Win32). + +## 6.3. Выравнивание + +- **NRes**: данные каждого ресурса выровнены по границе **8 байт** (0-padding между файлами). +- **RsLi**: выравнивание данных не описано в коде (данные идут подряд). + +## 6.4. Размер записей на диске + +- **NRes**: каталог — **64 байта** на запись, расположен в конце файла. +- **RsLi**: таблица — **32 байта** на запись (зашифрованная), расположена в начале файла (сразу после 32-байтного заголовка). + +## 6.5. Кэширование и memory mapping + +Оба формата используют Windows Memory-Mapped Files (`CreateFileMapping` + `MapViewOfFile`). NRes-архивы организованы в глобальный **связный список** (`dword_1003A66C`) со счётчиком ссылок и таймером неактивности (10 секунд = 0x2710 мс). При refcount == 0 и истечении таймера архив автоматически выгружается (если не установлен флаг `is_cacheable`). + +## 6.6. Размер seed XOR + +- **Заголовок RsLi**: seed — **4 байта** (DWORD) по смещению 20, но используются только младшие 2 байта (`lo = byte[0]`, `hi = byte[1]`). +- **Запись RsLi**: sort_to_original[i] — **2 байта** (int16) по смещению 18 записи. +- **Данные при комбинированном XOR+LZSS**: seed — **4 байта** (DWORD) из поля по смещению 20 записи, но опять используются только 2 байта. @@ -23,11 +23,7 @@ theme: nav: - Home: index.md - Specs: - - Assets: - - NRes: - - Документация по формату: specs/assets/nres/overview.md - - FRES Декомпрессия: specs/assets/nres/fres_decompression.md - - Huffman Декомпрессия: specs/assets/nres/huffman_decompression.md + - NRes / RsLi: specs/nres.md # Additional configuration extra: |
