feat: обновить заголовки разделов в документации по FXID и NRes для улучшения структуры
This commit was merged in pull request #4.
This commit is contained in:
@@ -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** архивов совпали побайтно.
|
||||
|
||||
Reference in New Issue
Block a user