From 54c94fddb5fcf4e38bc9124be0e9cec93a4cdcba Mon Sep 17 00:00:00 2001 From: Valentin Popov Date: Tue, 10 Feb 2026 00:30:25 +0400 Subject: Add detailed documentation for NRes and RsLi resource formats - Introduced a comprehensive markdown file `nres.md` detailing the structure, header, and operations of the NRes and RsLi formats. - Updated `mkdocs.yml` to reflect the new documentation structure, consolidating NRes and RsLi under a single entry. --- docs/specs/nres.md | 705 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 705 insertions(+) create mode 100644 docs/specs/nres.md (limited to 'docs/specs/nres.md') 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 байта. -- cgit v1.2.3