2026-02-10 00:30:25 +04:00
# Форматы игровых ресурсов
## Обзор
Библиотека `Ngi32.dll` реализует два различных формата архивов ресурсов:
1. **NRes ** — основной формат архива ресурсов, используемый через API `niOpenResFile` / `niCreateResFile` . Каталог файлов расположен в **конце ** файла. Поддерживает создание, редактирование, добавление и удаление записей.
2. **RsLi ** — формат библиотеки ресурсов, используемый через API `rsOpenLib` / `rsLoad` . Таблица записей расположена **в начале ** файла (сразу после заголовка) и зашифрована XOR-шифром. Поддерживает несколько методов сжатия. Только чтение.
---
2026-02-11 22:10:43 +00:00
## Часть 1. Формат NRes
2026-02-10 00:30:25 +04:00
2026-02-11 22:10:43 +00:00
### 1.1. Общая структура файла
2026-02-10 00:30:25 +04:00
```
┌──────────────────────────┐ Смещение 0
│ Заголовок (16 байт) │
├──────────────────────────┤ Смещение 16
│ │
│ Данные ресурсов │
│ (выровнены по 8 байт) │
│ │
├──────────────────────────┤ Смещение = total_size - entry_count × 64
│ Каталог записей │
│ (entry_count × 64 байт) │
└──────────────────────────┘ Смещение = total_size
```
2026-02-11 22:10:43 +00:00
### 1.2. Заголовок файла (16 байт)
2026-02-10 00:30:25 +04:00
| Смещение | Размер | Тип | Значение | Описание |
| -------- | ------ | ------- | ------------------- | ------------------------------------ |
| 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` ). Если значения не совпадают — файл отклоняется.
2026-02-11 22:10:43 +00:00
### 1.3. Положение каталога в файле
2026-02-10 00:30:25 +04:00
Каталог располагается в самом конце файла. Е г о смещение вычисляется по формуле:
```
directory_offset = total_size - entry_count × 64
```
Данные ресурсов занимают пространство между заголовком (16 байт) и каталогом.
2026-02-11 22:10:43 +00:00
### 1.4. Запись каталога (64 байта)
2026-02-10 00:30:25 +04:00
Каждая запись каталога занимает ровно **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 | Индекс сортировки (для двоичного поиска по имени) |
2026-02-11 22:10:43 +00:00
#### Поле «Имя файла» (смещение 20, 36 байт)
2026-02-10 00:30:25 +04:00
- Максимальная длина имени: **35 символов ** + 1 байт null-терминатор.
- При записи поле сначала обнуляется (`memset(0, 36 байт)` ), затем копируется имя (`strncpy` , макс. 35 символов).
- Поиск по имени выполняется **без учёта регистра ** (`_strcmpi` ).
2026-02-11 22:10:43 +00:00
#### Поле «Индекс сортировки» (смещение 60)
2026-02-10 00:30:25 +04:00
Используется для **двоичного поиска по имени ** . Содержит индекс оригинальной записи, отсортированной в алфавитном порядке (регистронезависимо). Индекс строится при сохранении файла функцией `sub_10013260` с помощью **пузырьковой сортировки ** по именам.
**Алгоритм поиска** (`sub_10011E60` ): классический двоичный поиск по отсортированному массиву индексов. Возвращает оригинальный индекс записи или `-1` при отсутствии.
2026-02-11 22:10:43 +00:00
#### Поле «Смещение данных» (смещение 56)
2026-02-10 00:30:25 +04:00
Абсолютное смещение от начала файла. Данные читаются из mapped view: `pointer = mapped_base + data_offset` .
2026-02-11 22:10:43 +00:00
### 1.5. Выравнивание данных
2026-02-10 00:30:25 +04:00
При добавлении р е с у р с а е г о данные записываются последовательно, после чего выполняется **выравнивание по 8-байтной границе ** :
```c
padding = ((data_size + 7) & ~7) - data_size;
// Если padding > 0, записываются нулевые байты
```
Таким образом, каждый блок данных начинается с адреса, кратного 8.
При изменении размера данных р е с у р с а выполняется сдвиг всех последующих данных и обновление смещений всех затронутых записей каталога.
2026-02-11 22:10:43 +00:00
### 1.6. Создание файла (API `niCreateResFile`)
2026-02-10 00:30:25 +04:00
При создании нового файла:
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. Каталог записей записывается в конец файла.
2026-02-11 22:10:43 +00:00
### 1.7. Режимы сортировки каталога
2026-02-10 00:30:25 +04:00
Функция `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, имя) |
2026-02-11 22:10:43 +00:00
### 1.8. Операция `niOpenResFileEx` — флаги открытия
2026-02-10 00:30:25 +04:00
Второй параметр — битовые флаги:
| Бит | Маска | Описание |
| --- | ----- | ----------------------------------------------------------------------------------- |
| 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, трактовать весь файл как единый р е с у р с |
2026-02-11 22:10:43 +00:00
### 1.9. Виртуальное касание страниц
2026-02-10 00:30:25 +04:00
Функция `sub_100197D0` выполняет «касание» страниц памяти для принудительной загрузки из memory-mapped файла. Она обходит адресное пространство с шагом 4096 байт (размер страницы), начиная с 0x10000 (64 КБ):
```
for (result = 0x10000; result < size; result += 4096);
```
Вызывается при чтении данных р е с у р с а с флагом `a3 != 0` для предзагрузки данных в оперативную память.
---
2026-02-11 22:10:43 +00:00
## Часть 2. Формат RsLi
2026-02-10 00:30:25 +04:00
2026-02-11 22:10:43 +00:00
### 2.1. Общая структура файла
2026-02-10 00:30:25 +04:00
```
┌───────────────────────────────┐ Смещение 0
│ Заголовок файла (32 байта) │
├───────────────────────────────┤ Смещение 32
│ Таблица записей (зашифрована)│
│ (entry_count × 32 байт) │
├───────────────────────────────┤ Смещение 32 + entry_count × 32
│ │
│ Данные ресурсов │
│ │
├───────────────────────────────┤
│ [Опциональный трейлер — 6 б ] │
└───────────────────────────────┘
```
2026-02-11 22:10:43 +00:00
### 2.2. Заголовок файла (32 байта)
2026-02-10 00:30:25 +04:00
| Смещение | Размер | Тип | Значение | Описание |
| -------- | ------ | ------- | ----------------- | --------------------------------------------- |
| 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 | — | — | Зарезервировано |
2026-02-11 22:10:43 +00:00
#### Флаг предсортировки (смещение 14)
2026-02-10 00:30:25 +04:00
- Если `*(uint16*)(header + 14) == 0xABBA` — движок **не строит ** таблицу индексов в памяти. Значения `entry[i].sort_to_original` используются **как есть ** (и для двоичного поиска, и как XOR‑ ключ для данных).
- Если значение **отлично от 0xABBA ** — после загрузки выполняется **пузырьковая сортировка ** имён и строится перестановка `sort_to_original[]` , которая затем **записывается в `entry[i].sort_to_original` ** , перетирая значения из файла. Именно эта перестановка далее используется и для поиска, и как XOR‑ ключ (младшие 16 бит).
2026-02-11 22:10:43 +00:00
### 2.3. XOR-шифр таблицы записей
2026-02-10 00:30:25 +04:00
Таблица записей начинается с о смещения 32 и зашифрована поточным XOR-шифром. Ключ инициализируется из DWORD по смещению 20 заголовка.
2026-02-11 22:10:43 +00:00
#### Начальное состояние
2026-02-10 00:30:25 +04:00
```
seed = * (uint32 * )(header + 20)
lo = seed & 0xFF // Младший байт
hi = (seed >> 8) & 0xFF // Второй байт
```
2026-02-11 22:10:43 +00:00
#### Алгоритм дешифровки (побайтовый)
2026-02-10 00:30:25 +04:00
Для каждого зашифрованного байта `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), но с другим начальным ключом из записи.
2026-02-11 22:10:43 +00:00
### 2.4. Запись таблицы (32 байта, на диске, до дешифровки)
2026-02-10 00:30:25 +04:00
После дешифровки каждая запись имеет следующую структуру:
| Смещение | Размер | Тип | Описание |
| -------- | ------ | -------- | -------------------------------------------------------------- |
| 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` ) |
2026-02-11 22:10:43 +00:00
#### Имена ресурсов
2026-02-10 00:30:25 +04:00
- Поле `name[12]` копируется побайтно. Внутренне движок всегда имеет `\0` сразу после этих 12 байт (зарезервированные 4 байта в памяти принудительно обнуляются), поэтому имя **может быть длиной до 12 символов ** даже без `\0` внутри `name[12]` .
- Н а практике имена обычно **uppercase ASCII ** . `rsFind` приводит запрос к верхнему регистру (`_strupr` ) и сравнивает побайтно.
- `rsFind` копирует имя запроса `strncpy(..., 16)` и принудительно ставит `\0` в `Destination[15]` , поэтому запрос длиннее 15 символов будет усечён.
2026-02-11 22:10:43 +00:00
#### Поле `sort_to_original[i]` (смещение 18)
2026-02-10 00:30:25 +04:00
Это **не “свойство записи” ** , а элемент таблицы индексов, по которой `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` ).
2026-02-11 22:10:43 +00:00
### 2.5. Поле флагов (смещение 16 записи)
2026-02-10 00:30:25 +04:00
Биты поля флагов кодируют метод сжатия и дополнительные атрибуты:
```
Биты [8:5] (маска 0x1E0): Метод сжатия/шифрования
Бит [6] (маска 0x040): Флаг realloc (буфер декомпрессии может быть больше)
```
2026-02-11 22:10:43 +00:00
#### Методы сжатия (биты 8– 5, маска 0x1E0)
2026-02-10 00:30:25 +04:00
| Значение | 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.
2026-02-11 22:10:43 +00:00
#### Бит 0x40 (выделение +0x12 и последующее `realloc`)
2026-02-10 00:30:25 +04:00
Бит 0x40 проверяется отдельно (`flags & 0x40` ). Если он установлен, выходной буфер выделяется с запасом `+0x12` (18 байт), а после распаковки вызывается `realloc` для усечения до точного `unpacked_size` .
Важно: этот же бит входит в код методов 0x40/0x60, поэтому для них поведение “+0x12 и shrink” включено автоматически.
2026-02-11 22:10:43 +00:00
### 2.6. Размеры данных
2026-02-10 00:30:25 +04:00
В каждой записи на диске хранятся о б а значения:
- `unpacked_size` (смещение 20) — размер распакованных данных.
- `packed_size` (смещение 28) — размер упакованных данных (байт во входном потоке для выбранного метода).
Для метода 0x00 (без сжатия) обычно `packed_size == unpacked_size` .
`rsGetInfo` возвращает именно `unpacked_size` (то, сколько байт выдаст `rsLoad` ).
2026-02-10 01:58:16 +04:00
Практический нюанс для метода `0x100` (Deflate): в реальных игровых данных встречается запись, где `packed_size` указывает на диапазон до `EOF + 1` . Поток успешно декодируется и без последнего байта; это похоже на lookahead-поведение декодера.
2026-02-11 22:10:43 +00:00
### 2.7. Опциональный трейлер медиа (6 байт)
2026-02-10 00:30:25 +04:00
При открытии с флагом `a2 & 2` :
| Смещение от конца | Размер | Тип | Описание |
| ----------------- | ------ | ------- | ----------------------- |
| − 6 | 2 | char[2] | Сигнатура `AO` (0x4F41) |
| − 4 | 4 | uint32 | Смещение медиа-оверлея |
Если трейлер присутствует, все смещения данных в записях корректируются: `effective_offset = entry_offset + media_overlay_offset` .
---
2026-02-11 22:10:43 +00:00
## Часть 3. Алгоритмы сжатия (формат RsLi)
2026-02-10 00:30:25 +04:00
2026-02-11 22:10:43 +00:00
### 3.1. XOR-шифр данных (метод 0x20)
2026-02-10 00:30:25 +04:00
Алгоритм идентичен XOR‑ шифр у таблицы записей (раздел 2.3), но начальный ключ берётся из `entry[i].sort_to_original` (смещение 18 записи, младшие 16 бит).
Важно про размер входа:
- В ветке **0x20 ** движок XOR‑ ит ровно `unpacked_size` байт (и ожидает, что поток данных имеет ту же длину; на практике `packed_size == unpacked_size` ).
- В ветках **0x60/0xA0 ** XOR применяется к **упакованному ** потоку длиной `packed_size` перед декомпрессией.
2026-02-11 22:10:43 +00:00
#### Инициализация
2026-02-10 00:30:25 +04:00
```
key16 = (uint16)entry.sort_to_original // int16 на диске по смещению 18
lo = key16 & 0xFF
hi = (key16 >> 8) & 0xFF
```
2026-02-11 22:10:43 +00:00
#### Дешифровка (псевдокод)
2026-02-10 00:30:25 +04:00
```
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
```
2026-02-11 22:10:43 +00:00
### 3.2. LZSS — простой вариант (метод 0x40)
2026-02-10 00:30:25 +04:00
Классический алгоритм LZSS (Lempel-Ziv-Storer-Szymanski) с кольцевым буфером.
2026-02-11 22:10:43 +00:00
#### Параметры
2026-02-10 00:30:25 +04:00
| Параметр | Значение |
| ----------------------------- | ------------------ |
| Размер кольцевого буфера | 4096 байт (0x1000) |
| Начальная позиция записи | 4078 (0xFEE) |
| Начальное заполнение | 0x20 (пробел) |
| Минимальная длина совпадения | 3 |
| Максимальная длина совпадения | 18 (4 бита + 3) |
2026-02-11 22:10:43 +00:00
#### Алгоритм декомпрессии
2026-02-10 00:30:25 +04:00
```
Инициализация:
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
2026-02-10 01:58:16 +04:00
- offset = low_byte | ((high_byte & 0xF0) << 4) // 12 бит
- length = (high_byte & 0x0F) + 3 // 4 бита + 3
2026-02-10 00:30:25 +04:00
- Скопировать 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
```
2026-02-11 22:10:43 +00:00
#### Подробная раскладка пары ссылки (2 байта)
2026-02-10 00:30:25 +04:00
```
Байт 0 (low): OOOOOOOO (биты [7:0] смещения)
2026-02-10 01:58:16 +04:00
Байт 1 (high): OOOOLLLL O = биты [11:8] смещения, L = длина − 3
2026-02-10 00:30:25 +04:00
2026-02-10 01:58:16 +04:00
offset = low | ((high & 0xF0) << 4) // Диапазон: 0– 4095
length = (high & 0x0F) + 3 // Диапазон: 3– 18
2026-02-10 00:30:25 +04:00
```
2026-02-11 22:10:43 +00:00
### 3.3. LZSS с адаптивным кодированием Хаффмана (метод 0x80)
2026-02-10 00:30:25 +04:00
Расширенный вариант LZSS, где литералы и длины совпадений кодируются с помощью адаптивного дерева Хаффмана.
2026-02-11 22:10:43 +00:00
#### Параметры
2026-02-10 00:30:25 +04:00
| Параметр | Значение |
| -------------------------------- | ------------------------------ |
| Размер кольцевого буфера | 4096 байт |
| Начальная позиция записи | **4036 ** (0xFC4) |
| Начальное заполнение | 0x20 (пробел) |
| Количество листовых узлов дерева | 314 |
| Символы литералов | 0– 255 (байты) |
| Символы длин | 256– 313 (длина = символ − 253) |
| Начальная длина | 3 (при символе 256) |
| Максимальная длина | 60 (при символе 313) |
2026-02-11 22:10:43 +00:00
#### Дерево Хаффмана
2026-02-10 00:30:25 +04:00
Дерево строится как **адаптивное ** (dynamic, self-adjusting):
- **627 узлов**: 314 листовых + 313 внутренних.
- В с е листья изначально имеют **вес 1 ** .
- Корень дерева — узел с индексом 0 (в массиве `parent` ).
- После декодирования каждого символа дерево **обновляется ** (функция `sub_1001B0AE` ): вес узла инкрементируется, и при нарушении порядка узлы **переставляются ** для поддержания свойства.
- При достижении суммарного веса **0x8000 (32768) ** — все веса **делятся на 2 ** (с округлением вверх) и дерево полностью перестраивается.
2026-02-11 22:10:43 +00:00
#### Кодирование позиции
2026-02-10 00:30:25 +04:00
Позиция в кольцевом буфере кодируется с помощью **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 }
```
2026-02-11 22:10:43 +00:00
#### Алгоритм декомпрессии (высокоуровневый)
2026-02-10 00:30:25 +04:00
```
Инициализация:
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. Если выходной буфер заполнен → завершить
```
2026-02-11 22:10:43 +00:00
### 3.4. XOR + LZSS (методы 0x60 и 0xA0)
2026-02-10 00:30:25 +04:00
Комбинированный метод: сначала XOR-дешифровка, затем LZSS-декомпрессия.
2026-02-11 22:10:43 +00:00
#### Алгоритм
2026-02-10 00:30:25 +04:00
1. Выделить временный буфер размером `compressed_size` (поле из записи, смещение 28).
2. Дешифровать сжатые данные XOR-шифром (раздел 3.1) с ключом из записи во временный буфер.
3. Применить LZSS-декомпрессию (простую или с Хаффманом, в зависимости от конкретного метода) из временного буфера в выходной.
4. Освободить временный буфер.
- **0x60** — XOR + простой LZSS (раздел 3.2)
- **0xA0** — XOR + LZSS с Хаффманом (раздел 3.3)
2026-02-11 22:10:43 +00:00
#### Начальное состояние XOR для данных
2026-02-10 00:30:25 +04:00
При комбинированном методе seed берётся из поля по смещению 20 записи (4-байтный). Однако ключ обрабатывается как 16-битный: `lo = seed & 0xFF` , `hi = (seed >> 8) & 0xFF` .
2026-02-11 22:10:43 +00:00
### 3.5. Deflate (метод 0x100)
2026-02-10 00:30:25 +04:00
Полноценная реализация алгоритма **Deflate ** (RFC 1951) с блочной структурой.
2026-02-11 22:10:43 +00:00
#### Общая структура
2026-02-10 00:30:25 +04:00
Данные состоят из последовательности блоков. Каждый блок начинается с :
- **1 бит** — `is_final` : признак последнего блока
- **2 бита** — `block_type` : тип блока
2026-02-11 22:10:43 +00:00
#### Типы блоков
2026-02-10 00:30:25 +04:00
| block_type | Описание | Функция |
| ---------- | --------------------------- | ---------------- |
| 0 | Без сжатия (stored) | `sub_1001A750` |
| 1 | Фиксированные коды Хаффмана | `sub_1001A8C0` |
| 2 | Динамические коды Хаффмана | `sub_1001AA30` |
| 3 | Зарезервировано (ошибка) | Возвращает код 2 |
2026-02-11 22:10:43 +00:00
#### Блок типа 0 (stored)
2026-02-10 00:30:25 +04:00
1. Отбросить оставшиеся биты до границы байта (выравнивание).
2. Прочитать 16 бит — `LEN` (длина блока).
3. Прочитать 16 бит — `NLEN` (дополнение длины, `NLEN == ~LEN & 0xFFFF` ).
4. Проверить: `LEN == (uint16)(~NLEN)` . При несовпадении — ошибка.
5. Скопировать `LEN` байт из входного потока в выходной.
Декомпрессор использует внутренний буфер размером **32768 байт ** (0x8000). При заполнении — промежуточная запись результата.
2026-02-11 22:10:43 +00:00
#### Блок типа 1 (фиксированные коды)
2026-02-10 00:30:25 +04:00
Стандартные коды Deflate:
- Литералы/длины: 288 кодов
- 0– 143: 8-битные коды
- 144– 255: 9-битные коды
- 256– 279: 7-битные коды
- 280– 287: 8-битные коды
- Дистанции: 30 кодов, все 5-битные
Используются предопределённые таблицы длин и дистанций (`unk_100370AC` , `unk_1003712C` и соответствующие экстра-биты).
2026-02-11 22:10:43 +00:00
#### Блок типа 2 (динамические коды)
2026-02-10 00:30:25 +04:00
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` .
2026-02-11 22:10:43 +00:00
#### Валидации
2026-02-10 00:30:25 +04:00
- `HLIT + 257 <= 286` (max 0x11E)
- `HDIST + 1 <= 30` (max 0x1E)
- При нарушении — возвращается ошибка 1.
2026-02-11 22:10:43 +00:00
### 3.6. Метод 0x00 (без сжатия)
2026-02-10 00:30:25 +04:00
Данные копируются «как есть» напрямую из файла. Вызывается через указатель на функцию `dword_1003A1B8` (фактически `memcpy` или аналог).
---
2026-02-11 22:10:43 +00:00
## Часть 4. Внутренние структуры в памяти
2026-02-10 00:30:25 +04:00
2026-02-11 22:10:43 +00:00
### 4.1. Внутренняя структура NRes-архива (opened, 0x68 байт = 104)
2026-02-10 00:30:25 +04:00
```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
};
```
2026-02-11 22:10:43 +00:00
### 4.2. Внутренняя структура RsLi-архива (56 + 64 × N байт)
2026-02-10 00:30:25 +04:00
```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 байта каждая
```
2026-02-11 22:10:43 +00:00
#### Внутренняя запись RsLi (64 байта)
2026-02-10 00:30:25 +04:00
```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: дополнительные поля
};
```
---
2026-02-11 22:10:43 +00:00
## Часть 5. Экспортируемые API-функции
2026-02-10 00:30:25 +04:00
2026-02-11 22:10:43 +00:00
### 5.1. NRes API
2026-02-10 00:30:25 +04:00
| Функция | Описание |
| ------------------------------ | ------------------------------------------------------------------------- |
| `niOpenResFile(path)` | Открыть NRes-архив (только чтение), эквивалент `niOpenResFileEx(path, 0)` |
| `niOpenResFileEx(path, flags)` | Открыть NRes-архив с флагами |
| `niOpenResInMem(ptr, size)` | Открыть NRes-архив из памяти |
| `niCreateResFile(path)` | Создать/открыть NRes-архив для записи |
2026-02-11 22:10:43 +00:00
### 5.2. RsLi API
2026-02-10 00:30:25 +04:00
| Функция | Описание |
| ------------------------------- | -------------------------------------------------------- |
| `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)` | Получить размер выделенного блока |
---
2026-02-11 22:10:43 +00:00
## Часть 6. Контрольные заметки для реализации
2026-02-10 00:30:25 +04:00
2026-02-11 22:10:43 +00:00
### 6.1. Кодировки и регистр
2026-02-10 00:30:25 +04:00
- **NRes**: имена хранятся **как есть ** (case-insensitive при поиске через `_strcmpi` ).
- **RsLi**: имена хранятся в **верхнем регистре ** . Перед поиском запрос приводится к верхнему регистру (`_strupr` ). Сравнение — через `strcmp` (case-sensitive для уже uppercase строк).
2026-02-11 22:10:43 +00:00
### 6.2. Порядок байт
2026-02-10 00:30:25 +04:00
В с е значения хранятся в **little-endian ** порядке (платформа x86/Win32).
2026-02-11 22:10:43 +00:00
### 6.3. Выравнивание
2026-02-10 00:30:25 +04:00
- **NRes**: данные каждого р е с у р с а выровнены по границе **8 байт ** (0-padding между файлами).
- **RsLi**: выравнивание данных не описано в коде (данные идут подряд).
2026-02-11 22:10:43 +00:00
### 6.4. Размер записей на диске
2026-02-10 00:30:25 +04:00
- **NRes**: каталог — **64 байта ** на запись, расположен в конце файла.
- **RsLi**: таблица — **32 байта ** на запись (зашифрованная), расположена в начале файла (сразу после 32-байтного заголовка).
2026-02-11 22:10:43 +00:00
### 6.5. Кэширование и memory mapping
2026-02-10 00:30:25 +04:00
О б а формата используют Windows Memory-Mapped Files (`CreateFileMapping` + `MapViewOfFile` ). NRes-архивы организованы в глобальный **связный список ** (`dword_1003A66C` ) с о счётчиком ссылок и таймером неактивности (10 секунд = 0x2710 мс). При refcount == 0 и истечении таймера архив автоматически выгружается (если не установлен флаг `is_cacheable` ).
2026-02-11 22:10:43 +00:00
### 6.6. Размер seed XOR
2026-02-10 00:30:25 +04:00
- **Заголовок 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 байта.
2026-02-10 01:58:16 +04:00
2026-02-11 22:10:43 +00:00
### 6.7. Эмпирическая проверка на данных игры
2026-02-10 01:58:16 +04:00
- Найдено архивов по сигнатуре: **122 ** (`NRes` : 120, `RsLi` : 2).
- Выполнен полный roundtrip `unpack -> pack -> byte-compare` : **122/122 ** архивов совпали побайтно.
- Для `RsLi` в проверенном наборе встретились методы: `0x040` и `0x100` .
Подтверждённые нюансы:
- Для LZSS (метод `0x040` ) рабочая раскладка нибблов в ссылке: `OOOO LLLL` , а не `LLLL OOOO` .
- Для Deflate (метод `0x100` ) возможен случай `packed_size == фа ктиче с кий_ко не ц + 1` на последней записи файла.