From 615891d550b23701aac4ac8cb7a475104d0a9592 Mon Sep 17 00:00:00 2001 From: Valentin Popov Date: Wed, 11 Feb 2026 22:10:43 +0000 Subject: feat: обновить заголовки разделов в документации по FXID и NRes для улучшения структуры MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/specs/fxid.md | 64 ++++++++++++------------- docs/specs/nres.md | 134 ++++++++++++++++++++++++++--------------------------- 2 files changed, 99 insertions(+), 99 deletions(-) diff --git a/docs/specs/fxid.md b/docs/specs/fxid.md index 957f95b..7dd1d4b 100644 --- a/docs/specs/fxid.md +++ b/docs/specs/fxid.md @@ -35,7 +35,7 @@ ## 2. Контейнер и runtime API -## 2.1. NRes entry +### 2.1. NRes entry FXID хранится как NRes-entry: @@ -45,7 +45,7 @@ FXID хранится как NRes-entry: - `attr1 = 0`, `attr2 = 0`, `attr3 = 1`. -## 2.2. Export API `Effect.dll` +### 2.2. Export API `Effect.dll` Экспортируются: @@ -54,7 +54,7 @@ FXID хранится как NRes-entry: `CreateFxManager` создаёт manager-объект (`0xB8` байт), инициализирует через `sub_10003AE0`, возвращает интерфейсный указатель (`base + 4`). -## 2.3. Интерфейс менеджера +### 2.3. Интерфейс менеджера Рабочая vtable (`off_1001E478`): @@ -83,7 +83,7 @@ FXID хранится как NRes-entry: Все значения little-endian. -## 3.1. Header (60 байт, `0x3C`) +### 3.1. Header (60 байт, `0x3C`) ```c struct FxHeader60 { @@ -107,7 +107,7 @@ struct FxHeader60 { Командный поток начинается строго с `offset = 0x3C`. -## 3.2. Header-поля (подтвержденная семантика) +### 3.2. Header-поля (подтвержденная семантика) - `cmd_count`: число команд (engine итерирует ровно столько шагов). - `time_mode`: базовый режим вычисления alpha/time (`sub_10005C60`). @@ -119,7 +119,7 @@ struct FxHeader60 { - `pivot_*`: используется в ветках `sub_10007D10`. - `scale_*`: копируется в runtime scale и влияет на матрицы. -## 3.3. `flags` (битовая карта) +### 3.3. `flags` (битовая карта) | Бит | Маска | Наблюдаемое поведение | |---|---:|---| @@ -137,7 +137,7 @@ struct FxHeader60 { Нерасшифрованные биты должны сохраняться 1:1. -## 3.4. `time_mode` (`0..17`) +### 3.4. `time_mode` (`0..17`) Обозначения (`sub_10005C60`): @@ -175,7 +175,7 @@ Post-обработка после mode: ## 4. Командный поток -## 4.1. Общий формат команды +### 4.1. Общий формат команды Каждая команда: @@ -190,7 +190,7 @@ Post-обработка после mode: Выравнивания между командами нет. -## 4.2. Размеры +### 4.2. Размеры | Opcode | Размер записи | |---:|---:| @@ -205,7 +205,7 @@ Post-обработка после mode: | 9 | 208 | | 10 | 208 | -## 4.3. Opcode -> runtime-класс (vtable) +### 4.3. Opcode -> runtime-класс (vtable) | Opcode | `new(size)` | vtable | |---:|---:|---| @@ -220,7 +220,7 @@ Post-обработка после mode: | 9 | `0x100` | `off_1001E700` | | 10 | `0x48` | `off_1001E24C` | -## 4.4. Общий вызовной контракт команды +### 4.4. Общий вызовной контракт команды После создания команды (`sub_10007650`): @@ -332,7 +332,7 @@ struct ResourceRef64 { Смещения указаны от начала команды (включая `cmd_word`). -## 8.1. Opcode 1 (`off_1001E78C`, size=224) +### 8.1. Opcode 1 (`off_1001E78C`, size=224) Основные методы: @@ -387,7 +387,7 @@ struct FxCmd01 { - `6 -> create_kind=1, flags=0xA0000000`; - `7 -> create_kind=1, flags=0x20000000`. -## 8.2. Opcode 2 (`off_1001F048`, size=148) +### 8.2. Opcode 2 (`off_1001F048`, size=148) Основные методы: @@ -424,7 +424,7 @@ struct FxCmd02 { - `0 -> 0`, `1 -> 512`, `2 -> 2`, `3 -> 514`. -## 8.3. Opcode 3 (`off_1001E770`, size=200) +### 8.3. Opcode 3 (`off_1001E770`, size=200) Методы: @@ -465,7 +465,7 @@ struct FxCmd03 { }; ``` -## 8.4. Opcode 4 (`off_1001E754`, size=204) +### 8.4. Opcode 4 (`off_1001E754`, size=204) Layout как opcode 3 + последний коэффициент: @@ -478,7 +478,7 @@ struct FxCmd04 { `sub_100108C0`: `obj->inv = 1.0 / raw[200]`. -## 8.5. Opcode 5 (`off_1001E360`, size=112) +### 8.5. Opcode 5 (`off_1001E360`, size=112) Методы: @@ -508,7 +508,7 @@ struct FxCmd05 { }; ``` -## 8.6. Opcode 6 (`off_1001E738`, size=4) +### 8.6. Opcode 6 (`off_1001E738`, size=4) Только `cmd_word`: @@ -520,7 +520,7 @@ struct FxCmd06 { `init/update/emit` фактически no-op (`sub_100030B0` возвращает `0`). -## 8.7. Opcode 7 (`off_1001E228`, size=208) +### 8.7. Opcode 7 (`off_1001E228`, size=208) Методы: @@ -563,7 +563,7 @@ struct FxCmd07 { }; ``` -## 8.8. Opcode 8 (`off_1001E71C`, size=248) +### 8.8. Opcode 8 (`off_1001E71C`, size=248) Методы: @@ -609,7 +609,7 @@ struct FxCmd08 { }; ``` -## 8.9. Opcode 9 (`off_1001E700`, size=208) +### 8.9. Opcode 9 (`off_1001E700`, size=208) Layout как opcode 3 с двумя final-полями: @@ -626,7 +626,7 @@ struct FxCmd09 { - init/update как у opcode 3 (`sub_100103B0`, `sub_100105F0`); - emit: `sub_100138C0` -> формирует код рендера и вызывает `sub_100106C0`. -## 8.10. Opcode 10 (`off_1001E24C`, size=208) +### 8.10. Opcode 10 (`off_1001E24C`, size=208) Body-layout совпадает с opcode 7 (`FxCmd07`), но другой runtime класс. @@ -643,7 +643,7 @@ Body-layout совпадает с opcode 7 (`FxCmd07`), но другой runtim ## 9. Runtime-специфика по opcode (важные отличия) -## 9.1. Opcode 1 +### 9.1. Opcode 1 - создаёт handle через manager (`vfunc +48`); - задаёт флаги handle (`vfunc +52`); @@ -653,30 +653,30 @@ Body-layout совпадает с opcode 7 (`FxCmd07`), но другой runtim - 4-компонентный параметр (`vfunc +12`), - scalar+rgb (`vfunc +16`). -## 9.2. Opcode 2 +### 9.2. Opcode 2 - `ResourceRef64` резолвится через `sub_100065A0` (режим-зависимая загрузка, в данных обычно `sounds.lib`/`wav`); - использует manager-команду id `910`. -## 9.3. Opcode 3/4/9 +### 9.3. Opcode 3/4/9 - общий core-emitter в `sub_100106C0`; - opcode 4 добавляет нормализацию по `raw+200`; - opcode 9 добавляет переключение render-кода (`raw+200/+204`). -## 9.4. Opcode 5 +### 9.4. Opcode 5 - держит массив внутренних сегментов (`332` байта/элемент, ctor `sub_100099F0`); - context-matrix приходит через `vfunc +24` (`sub_10003070`). -## 9.5. Opcode 7/10 +### 9.5. Opcode 7/10 - общий update/render (`sub_10001230`, `sub_10001300`); - разные внутренние element-форматы: - opcode 7: `204` байта/элемент (`sub_100092D0`), - opcode 10: `492` байта/элемент (`sub_1000BB40`). -## 9.6. Opcode 8 +### 9.6. Opcode 8 - самый тяжёлый спавнер, хранит ring/slot-структуры; - emit фаза (`sub_10012030`) использует `mode`, `render_pow`, per-slot transforms. @@ -685,7 +685,7 @@ Body-layout совпадает с opcode 7 (`FxCmd07`), но другой runtim ## 10. Спецификация инструментов -## 10.1. Reader (strict) +### 10.1. Reader (strict) Алгоритм: @@ -699,14 +699,14 @@ Body-layout совпадает с opcode 7 (`FxCmd07`), но другой runtim - `ptr += size(opcode)`; 5. strict-tail: `ptr == len(payload)`. -## 10.2. Reader (engine-compatible) +### 10.2. Reader (engine-compatible) Legacy-режим (опасный, только при необходимости byte-совместимости): - без bounds-check; - tolerant к unknown opcode как в оригинале. -## 10.3. Writer (canonical) +### 10.3. Writer (canonical) 1. записать `FxHeader60`; 2. `cmd_count = commands.len()`; @@ -714,7 +714,7 @@ Legacy-режим (опасный, только при необходимост 4. размер payload: `0x3C + sum(size(op_i))`; 5. без хвостовых байт. -## 10.4. Editor (lossless) +### 10.4. Editor (lossless) Правила: @@ -724,7 +724,7 @@ Legacy-режим (опасный, только при необходимост - сохранять неизвестные биты (`cmd_word`, `header.flags`) copy-through; - для частично-известных полей поддерживать режим `opaque`. -## 10.5. IR/JSON (рекомендуемая форма) +### 10.5. IR/JSON (рекомендуемая форма) ```json { diff --git a/docs/specs/nres.md b/docs/specs/nres.md index 52f4f79..32ccb1b 100644 --- a/docs/specs/nres.md +++ b/docs/specs/nres.md @@ -10,9 +10,9 @@ --- -# Часть 1. Формат NRes +## Часть 1. Формат NRes -## 1.1. Общая структура файла +### 1.1. Общая структура файла ``` ┌──────────────────────────┐ Смещение 0 @@ -28,7 +28,7 @@ └──────────────────────────┘ Смещение = total_size ``` -## 1.2. Заголовок файла (16 байт) +### 1.2. Заголовок файла (16 байт) | Смещение | Размер | Тип | Значение | Описание | | -------- | ------ | ------- | ------------------- | ------------------------------------ | @@ -39,7 +39,7 @@ **Валидация при открытии:** магическая сигнатура и версия должны совпадать точно. Поле `total_size` (смещение 12) **проверяется на равенство** с фактическим размером файла (`GetFileSize`). Если значения не совпадают — файл отклоняется. -## 1.3. Положение каталога в файле +### 1.3. Положение каталога в файле Каталог располагается в самом конце файла. Его смещение вычисляется по формуле: @@ -49,7 +49,7 @@ directory_offset = total_size - entry_count × 64 Данные ресурсов занимают пространство между заголовком (16 байт) и каталогом. -## 1.4. Запись каталога (64 байта) +### 1.4. Запись каталога (64 байта) Каждая запись каталога занимает ровно **64 байта** (0x40): @@ -64,23 +64,23 @@ directory_offset = total_size - entry_count × 64 | 56 | 4 | uint32 | Смещение данных от начала файла | | 60 | 4 | uint32 | Индекс сортировки (для двоичного поиска по имени) | -### Поле «Имя файла» (смещение 20, 36 байт) +#### Поле «Имя файла» (смещение 20, 36 байт) - Максимальная длина имени: **35 символов** + 1 байт null-терминатор. - При записи поле сначала обнуляется (`memset(0, 36 байт)`), затем копируется имя (`strncpy`, макс. 35 символов). - Поиск по имени выполняется **без учёта регистра** (`_strcmpi`). -### Поле «Индекс сортировки» (смещение 60) +#### Поле «Индекс сортировки» (смещение 60) Используется для **двоичного поиска по имени**. Содержит индекс оригинальной записи, отсортированной в алфавитном порядке (регистронезависимо). Индекс строится при сохранении файла функцией `sub_10013260` с помощью **пузырьковой сортировки** по именам. **Алгоритм поиска** (`sub_10011E60`): классический двоичный поиск по отсортированному массиву индексов. Возвращает оригинальный индекс записи или `-1` при отсутствии. -### Поле «Смещение данных» (смещение 56) +#### Поле «Смещение данных» (смещение 56) Абсолютное смещение от начала файла. Данные читаются из mapped view: `pointer = mapped_base + data_offset`. -## 1.5. Выравнивание данных +### 1.5. Выравнивание данных При добавлении ресурса его данные записываются последовательно, после чего выполняется **выравнивание по 8-байтной границе**: @@ -93,7 +93,7 @@ padding = ((data_size + 7) & ~7) - data_size; При изменении размера данных ресурса выполняется сдвиг всех последующих данных и обновление смещений всех затронутых записей каталога. -## 1.6. Создание файла (API `niCreateResFile`) +### 1.6. Создание файла (API `niCreateResFile`) При создании нового файла: @@ -107,7 +107,7 @@ padding = ((data_size + 7) & ~7) - data_size; 3. Индексы сортировки пересчитываются. 4. Каталог записей записывается в конец файла. -## 1.7. Режимы сортировки каталога +### 1.7. Режимы сортировки каталога Функция `sub_10012560` поддерживает 12 режимов сортировки (0–11): @@ -126,7 +126,7 @@ padding = ((data_size + 7) & ~7) - data_size; | 10 | По (атрибут 1, имя) | | 11 | По (атрибут 2, имя) | -## 1.8. Операция `niOpenResFileEx` — флаги открытия +### 1.8. Операция `niOpenResFileEx` — флаги открытия Второй параметр — битовые флаги: @@ -137,7 +137,7 @@ padding = ((data_size + 7) & ~7) - data_size; | 2 | 0x04 | Пометить файл как «кэшируемый» (не выгружать при refcount=0) | | 3 | 0x08 | Raw-режим: не проверять заголовок NRes, трактовать весь файл как единый ресурс | -## 1.9. Виртуальное касание страниц +### 1.9. Виртуальное касание страниц Функция `sub_100197D0` выполняет «касание» страниц памяти для принудительной загрузки из memory-mapped файла. Она обходит адресное пространство с шагом 4096 байт (размер страницы), начиная с 0x10000 (64 КБ): @@ -149,9 +149,9 @@ for (result = 0x10000; result < size; result += 4096); --- -# Часть 2. Формат RsLi +## Часть 2. Формат RsLi -## 2.1. Общая структура файла +### 2.1. Общая структура файла ``` ┌───────────────────────────────┐ Смещение 0 @@ -168,7 +168,7 @@ for (result = 0x10000; result < size; result += 4096); └───────────────────────────────┘ ``` -## 2.2. Заголовок файла (32 байта) +### 2.2. Заголовок файла (32 байта) | Смещение | Размер | Тип | Значение | Описание | | -------- | ------ | ------- | ----------------- | --------------------------------------------- | @@ -182,16 +182,16 @@ for (result = 0x10000; result < size; result += 4096); | 20 | 4 | uint32 | — | **Начальное состояние XOR-шифра** (seed) | | 24 | 8 | — | — | Зарезервировано | -### Флаг предсортировки (смещение 14) +#### Флаг предсортировки (смещение 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-шифр таблицы записей +### 2.3. XOR-шифр таблицы записей Таблица записей начинается со смещения 32 и зашифрована поточным XOR-шифром. Ключ инициализируется из DWORD по смещению 20 заголовка. -### Начальное состояние +#### Начальное состояние ``` seed = *(uint32*)(header + 20) @@ -199,7 +199,7 @@ lo = seed & 0xFF // Младший байт hi = (seed >> 8) & 0xFF // Второй байт ``` -### Алгоритм дешифровки (побайтовый) +#### Алгоритм дешифровки (побайтовый) Для каждого зашифрованного байта `encrypted[i]`, начиная с `i = 0`: @@ -225,7 +225,7 @@ def decrypt_rs_entries(encrypted_data: bytes, seed: int) -> bytes: Этот же алгоритм используется для шифрования данных ресурсов с методом XOR (флаги 0x20, 0x60, 0xA0), но с другим начальным ключом из записи. -## 2.4. Запись таблицы (32 байта, на диске, до дешифровки) +### 2.4. Запись таблицы (32 байта, на диске, до дешифровки) После дешифровки каждая запись имеет следующую структуру: @@ -239,13 +239,13 @@ def decrypt_rs_entries(encrypted_data: bytes, seed: int) -> bytes: | 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) +#### Поле `sort_to_original[i]` (смещение 18) Это **не “свойство записи”**, а элемент таблицы индексов, по которой `rsFind` делает двоичный поиск: @@ -254,7 +254,7 @@ def decrypt_rs_entries(encrypted_data: bytes, seed: int) -> bytes: Поиск выполняется **двоичным поиском** по этой таблице, с фолбэком на **линейный поиск** если двоичный не нашёл (поведение `rsFind`). -## 2.5. Поле флагов (смещение 16 записи) +### 2.5. Поле флагов (смещение 16 записи) Биты поля флагов кодируют метод сжатия и дополнительные атрибуты: @@ -263,7 +263,7 @@ def decrypt_rs_entries(encrypted_data: bytes, seed: int) -> bytes: Бит [6] (маска 0x040): Флаг realloc (буфер декомпрессии может быть больше) ``` -### Методы сжатия (биты 8–5, маска 0x1E0) +#### Методы сжатия (биты 8–5, маска 0x1E0) | Значение | Hex | Описание | | -------- | ----- | --------------------------------------- | @@ -281,13 +281,13 @@ def decrypt_rs_entries(encrypted_data: bytes, seed: int) -> bytes: - для 0x60 вернётся 0x40, - для 0xA0 вернётся 0x80. -### Бит 0x40 (выделение +0x12 и последующее `realloc`) +#### Бит 0x40 (выделение +0x12 и последующее `realloc`) Бит 0x40 проверяется отдельно (`flags & 0x40`). Если он установлен, выходной буфер выделяется с запасом `+0x12` (18 байт), а после распаковки вызывается `realloc` для усечения до точного `unpacked_size`. Важно: этот же бит входит в код методов 0x40/0x60, поэтому для них поведение “+0x12 и shrink” включено автоматически. -## 2.6. Размеры данных +### 2.6. Размеры данных В каждой записи на диске хранятся оба значения: @@ -300,7 +300,7 @@ def decrypt_rs_entries(encrypted_data: bytes, seed: int) -> bytes: Практический нюанс для метода `0x100` (Deflate): в реальных игровых данных встречается запись, где `packed_size` указывает на диапазон до `EOF + 1`. Поток успешно декодируется и без последнего байта; это похоже на lookahead-поведение декодера. -## 2.7. Опциональный трейлер медиа (6 байт) +### 2.7. Опциональный трейлер медиа (6 байт) При открытии с флагом `a2 & 2`: @@ -313,9 +313,9 @@ def decrypt_rs_entries(encrypted_data: bytes, seed: int) -> bytes: --- -# Часть 3. Алгоритмы сжатия (формат RsLi) +## Часть 3. Алгоритмы сжатия (формат RsLi) -## 3.1. XOR-шифр данных (метод 0x20) +### 3.1. XOR-шифр данных (метод 0x20) Алгоритм идентичен XOR‑шифру таблицы записей (раздел 2.3), но начальный ключ берётся из `entry[i].sort_to_original` (смещение 18 записи, младшие 16 бит). @@ -324,7 +324,7 @@ def decrypt_rs_entries(encrypted_data: bytes, seed: int) -> bytes: - В ветке **0x20** движок XOR‑ит ровно `unpacked_size` байт (и ожидает, что поток данных имеет ту же длину; на практике `packed_size == unpacked_size`). - В ветках **0x60/0xA0** XOR применяется к **упакованному** потоку длиной `packed_size` перед декомпрессией. -### Инициализация +#### Инициализация ``` key16 = (uint16)entry.sort_to_original // int16 на диске по смещению 18 @@ -332,7 +332,7 @@ lo = key16 & 0xFF hi = (key16 >> 8) & 0xFF ``` -### Дешифровка (псевдокод) +#### Дешифровка (псевдокод) ``` for i in range(N): # N = unpacked_size (для 0x20) или packed_size (для 0x60/0xA0) @@ -341,11 +341,11 @@ for i in range(N): # N = unpacked_size (для 0x20) или pack hi = (lo ^ ((hi >> 1) & 0xFF)) & 0xFF ``` -## 3.2. LZSS — простой вариант (метод 0x40) +### 3.2. LZSS — простой вариант (метод 0x40) Классический алгоритм LZSS (Lempel-Ziv-Storer-Szymanski) с кольцевым буфером. -### Параметры +#### Параметры | Параметр | Значение | | ----------------------------- | ------------------ | @@ -355,7 +355,7 @@ for i in range(N): # N = unpacked_size (для 0x20) или pack | Минимальная длина совпадения | 3 | | Максимальная длина совпадения | 18 (4 бита + 3) | -### Алгоритм декомпрессии +#### Алгоритм декомпрессии ``` Инициализация: @@ -400,7 +400,7 @@ for i in range(N): # N = unpacked_size (для 0x20) или pack 4. flags_bits_remaining -= 1 ``` -### Подробная раскладка пары ссылки (2 байта) +#### Подробная раскладка пары ссылки (2 байта) ``` Байт 0 (low): OOOOOOOO (биты [7:0] смещения) @@ -410,11 +410,11 @@ offset = low | ((high & 0xF0) << 4) // Диапазон: 0–4095 length = (high & 0x0F) + 3 // Диапазон: 3–18 ``` -## 3.3. LZSS с адаптивным кодированием Хаффмана (метод 0x80) +### 3.3. LZSS с адаптивным кодированием Хаффмана (метод 0x80) Расширенный вариант LZSS, где литералы и длины совпадений кодируются с помощью адаптивного дерева Хаффмана. -### Параметры +#### Параметры | Параметр | Значение | | -------------------------------- | ------------------------------ | @@ -427,7 +427,7 @@ length = (high & 0x0F) + 3 // Диапазон: 3–18 | Начальная длина | 3 (при символе 256) | | Максимальная длина | 60 (при символе 313) | -### Дерево Хаффмана +#### Дерево Хаффмана Дерево строится как **адаптивное** (dynamic, self-adjusting): @@ -437,7 +437,7 @@ length = (high & 0x0F) + 3 // Диапазон: 3–18 - После декодирования каждого символа дерево **обновляется** (функция `sub_1001B0AE`): вес узла инкрементируется, и при нарушении порядка узлы **переставляются** для поддержания свойства. - При достижении суммарного веса **0x8000 (32768)** — все веса **делятся на 2** (с округлением вверх) и дерево полностью перестраивается. -### Кодирование позиции +#### Кодирование позиции Позиция в кольцевом буфере кодируется с помощью **d-кода** (таблица дистанций): @@ -455,7 +455,7 @@ length = (high & 0x0F) + 3 // Диапазон: 3–18 { 0x20, 0x30, 0x40, 0x30, 0x30, 0x10 } ``` -### Алгоритм декомпрессии (высокоуровневый) +#### Алгоритм декомпрессии (высокоуровневый) ``` Инициализация: @@ -489,11 +489,11 @@ length = (high & 0x0F) + 3 // Диапазон: 3–18 5. Если выходной буфер заполнен → завершить ``` -## 3.4. XOR + LZSS (методы 0x60 и 0xA0) +### 3.4. XOR + LZSS (методы 0x60 и 0xA0) Комбинированный метод: сначала XOR-дешифровка, затем LZSS-декомпрессия. -### Алгоритм +#### Алгоритм 1. Выделить временный буфер размером `compressed_size` (поле из записи, смещение 28). 2. Дешифровать сжатые данные XOR-шифром (раздел 3.1) с ключом из записи во временный буфер. @@ -503,22 +503,22 @@ length = (high & 0x0F) + 3 // Диапазон: 3–18 - **0x60** — XOR + простой LZSS (раздел 3.2) - **0xA0** — XOR + LZSS с Хаффманом (раздел 3.3) -### Начальное состояние XOR для данных +#### Начальное состояние XOR для данных При комбинированном методе seed берётся из поля по смещению 20 записи (4-байтный). Однако ключ обрабатывается как 16-битный: `lo = seed & 0xFF`, `hi = (seed >> 8) & 0xFF`. -## 3.5. Deflate (метод 0x100) +### 3.5. Deflate (метод 0x100) Полноценная реализация алгоритма **Deflate** (RFC 1951) с блочной структурой. -### Общая структура +#### Общая структура Данные состоят из последовательности блоков. Каждый блок начинается с: - **1 бит** — `is_final`: признак последнего блока - **2 бита** — `block_type`: тип блока -### Типы блоков +#### Типы блоков | block_type | Описание | Функция | | ---------- | --------------------------- | ---------------- | @@ -527,7 +527,7 @@ length = (high & 0x0F) + 3 // Диапазон: 3–18 | 2 | Динамические коды Хаффмана | `sub_1001AA30` | | 3 | Зарезервировано (ошибка) | Возвращает код 2 | -### Блок типа 0 (stored) +#### Блок типа 0 (stored) 1. Отбросить оставшиеся биты до границы байта (выравнивание). 2. Прочитать 16 бит — `LEN` (длина блока). @@ -537,7 +537,7 @@ length = (high & 0x0F) + 3 // Диапазон: 3–18 Декомпрессор использует внутренний буфер размером **32768 байт** (0x8000). При заполнении — промежуточная запись результата. -### Блок типа 1 (фиксированные коды) +#### Блок типа 1 (фиксированные коды) Стандартные коды Deflate: @@ -550,7 +550,7 @@ length = (high & 0x0F) + 3 // Диапазон: 3–18 Используются предопределённые таблицы длин и дистанций (`unk_100370AC`, `unk_1003712C` и соответствующие экстра-биты). -### Блок типа 2 (динамические коды) +#### Блок типа 2 (динамические коды) 1. Прочитать 5 бит → `HLIT` (количество литералов/длин − 257). Диапазон: 257–286. 2. Прочитать 5 бит → `HDIST` (количество дистанций − 1). Диапазон: 1–30. @@ -569,21 +569,21 @@ length = (high & 0x0F) + 3 // Диапазон: 3–18 Хранится в `dword_10037060`. -### Валидации +#### Валидации - `HLIT + 257 <= 286` (max 0x11E) - `HDIST + 1 <= 30` (max 0x1E) - При нарушении — возвращается ошибка 1. -## 3.6. Метод 0x00 (без сжатия) +### 3.6. Метод 0x00 (без сжатия) Данные копируются «как есть» напрямую из файла. Вызывается через указатель на функцию `dword_1003A1B8` (фактически `memcpy` или аналог). --- -# Часть 4. Внутренние структуры в памяти +## Часть 4. Внутренние структуры в памяти -## 4.1. Внутренняя структура NRes-архива (opened, 0x68 байт = 104) +### 4.1. Внутренняя структура NRes-архива (opened, 0x68 байт = 104) ```c struct NResArchive { // Размер: 0x68 (104 байта) @@ -601,7 +601,7 @@ struct NResArchive { // Размер: 0x68 (104 байта) }; ``` -## 4.2. Внутренняя структура RsLi-архива (56 + 64 × N байт) +### 4.2. Внутренняя структура RsLi-архива (56 + 64 × N байт) ```c struct RsLibHeader { // 56 байт (14 DWORD) @@ -623,7 +623,7 @@ struct RsLibHeader { // 56 байт (14 DWORD) // Далее следуют entry_count записей по 64 байта каждая ``` -### Внутренняя запись RsLi (64 байта) +#### Внутренняя запись RsLi (64 байта) ```c struct RsLibEntry { // 64 байта (16 DWORD) @@ -643,9 +643,9 @@ struct RsLibEntry { // 64 байта (16 DWORD) --- -# Часть 5. Экспортируемые API-функции +## Часть 5. Экспортируемые API-функции -## 5.1. NRes API +### 5.1. NRes API | Функция | Описание | | ------------------------------ | ------------------------------------------------------------------------- | @@ -654,7 +654,7 @@ struct RsLibEntry { // 64 байта (16 DWORD) | `niOpenResInMem(ptr, size)` | Открыть NRes-архив из памяти | | `niCreateResFile(path)` | Создать/открыть NRes-архив для записи | -## 5.2. RsLi API +### 5.2. RsLi API | Функция | Описание | | ------------------------------- | -------------------------------------------------------- | @@ -675,38 +675,38 @@ struct RsLibEntry { // 64 байта (16 DWORD) --- -# Часть 6. Контрольные заметки для реализации +## Часть 6. Контрольные заметки для реализации -## 6.1. Кодировки и регистр +### 6.1. Кодировки и регистр - **NRes**: имена хранятся **как есть** (case-insensitive при поиске через `_strcmpi`). - **RsLi**: имена хранятся в **верхнем регистре**. Перед поиском запрос приводится к верхнему регистру (`_strupr`). Сравнение — через `strcmp` (case-sensitive для уже uppercase строк). -## 6.2. Порядок байт +### 6.2. Порядок байт Все значения хранятся в **little-endian** порядке (платформа x86/Win32). -## 6.3. Выравнивание +### 6.3. Выравнивание - **NRes**: данные каждого ресурса выровнены по границе **8 байт** (0-padding между файлами). - **RsLi**: выравнивание данных не описано в коде (данные идут подряд). -## 6.4. Размер записей на диске +### 6.4. Размер записей на диске - **NRes**: каталог — **64 байта** на запись, расположен в конце файла. - **RsLi**: таблица — **32 байта** на запись (зашифрованная), расположена в начале файла (сразу после 32-байтного заголовка). -## 6.5. Кэширование и memory mapping +### 6.5. Кэширование и memory mapping Оба формата используют Windows Memory-Mapped Files (`CreateFileMapping` + `MapViewOfFile`). NRes-архивы организованы в глобальный **связный список** (`dword_1003A66C`) со счётчиком ссылок и таймером неактивности (10 секунд = 0x2710 мс). При refcount == 0 и истечении таймера архив автоматически выгружается (если не установлен флаг `is_cacheable`). -## 6.6. Размер seed XOR +### 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 байта. -## 6.7. Эмпирическая проверка на данных игры +### 6.7. Эмпирическая проверка на данных игры - Найдено архивов по сигнатуре: **122** (`NRes`: 120, `RsLi`: 2). - Выполнен полный roundtrip `unpack -> pack -> byte-compare`: **122/122** архивов совпали побайтно. -- cgit v1.2.3