Add detailed documentation for NRes and RsLi resource formats
Some checks failed
Test / cargo test (push) Failing after 41s
Some checks failed
Test / cargo test (push) Failing after 41s
- 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.
This commit is contained in:
20
.gitignore
vendored
20
.gitignore
vendored
@@ -64,9 +64,17 @@ $RECYCLE.BIN/
|
|||||||
# Windows shortcuts
|
# Windows shortcuts
|
||||||
*.lnk
|
*.lnk
|
||||||
|
|
||||||
# Zig programming language
|
# Generated by Cargo
|
||||||
zig-cache/
|
# will have compiled files and executables
|
||||||
zig-out/
|
debug/
|
||||||
build/
|
target/
|
||||||
build-*/
|
|
||||||
docgen_tmp/
|
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
|
||||||
|
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
|
||||||
|
Cargo.lock
|
||||||
|
|
||||||
|
# These are backup files generated by rustfmt
|
||||||
|
**/*.rs.bk
|
||||||
|
|
||||||
|
# MSVC Windows builds of rustc generate these, which store debugging information
|
||||||
|
*.pdb
|
||||||
@@ -1,402 +0,0 @@
|
|||||||
# FRES Декомпрессия
|
|
||||||
|
|
||||||
## Обзор
|
|
||||||
|
|
||||||
FRES — это гибридный алгоритм сжатия, использующий комбинацию RLE (Run-Length Encoding) и LZ77-подобного сжатия со скользящим окном. Существуют два режима работы: **adaptive Huffman** (флаг `a1 < 0`) и **простой битовый** (флаг `a1 >= 0`).
|
|
||||||
|
|
||||||
```c
|
|
||||||
char __stdcall sub_1001B22E(
|
|
||||||
char a1, // Флаг режима (< 0 = Huffman, >= 0 = простой)
|
|
||||||
int a2, // Ключ/seed (не используется напрямую)
|
|
||||||
_BYTE *a3, // Выходной буфер
|
|
||||||
int a4, // Размер выходного буфера
|
|
||||||
_BYTE *a5, // Входные сжатые данные
|
|
||||||
int a6 // Размер входных данных
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Структуры данных
|
|
||||||
|
|
||||||
### Глобальные переменные
|
|
||||||
|
|
||||||
```c
|
|
||||||
byte_1003A910[4096] // Циклический буфер скользящего окна (12 бит адрес)
|
|
||||||
dword_1003E09C // Указатель на конец выходного буфера
|
|
||||||
dword_1003E0A0 // Текущая позиция в циклическом буфере
|
|
||||||
dword_1003E098 // Состояние Huffman дерева
|
|
||||||
dword_1003E0A4 // Длина повтора для LZ77
|
|
||||||
```
|
|
||||||
|
|
||||||
### Константы
|
|
||||||
|
|
||||||
```c
|
|
||||||
#define WINDOW_SIZE 4096 // Размер скользящего окна (0x1000)
|
|
||||||
#define WINDOW_MASK 0x0FFF // Маска для циклического буфера
|
|
||||||
#define INIT_POS_NEG 4078 // Начальная позиция для Huffman режима
|
|
||||||
#define INIT_POS_POS 4036 // Начальная позиция для простого режима
|
|
||||||
```
|
|
||||||
|
|
||||||
## Режим 1: Простой битовый режим (a1 >= 0)
|
|
||||||
|
|
||||||
Это более простой режим без Huffman кодирования. Работает следующим образом:
|
|
||||||
|
|
||||||
### Алгоритм
|
|
||||||
|
|
||||||
```
|
|
||||||
Инициализация:
|
|
||||||
position = 4036
|
|
||||||
flags = 0
|
|
||||||
flagBits = 0
|
|
||||||
|
|
||||||
Цикл декомпрессии:
|
|
||||||
Пока есть входные данные и выходной буфер не заполнен:
|
|
||||||
|
|
||||||
1. Прочитать бит флага (LSB-first):
|
|
||||||
if (flagBits == 0):
|
|
||||||
flags = *input++
|
|
||||||
flagBits = 8
|
|
||||||
|
|
||||||
flag_bit = flags & 1
|
|
||||||
flags >>= 1
|
|
||||||
flagBits -= 1
|
|
||||||
|
|
||||||
2. Выбор действия по биту:
|
|
||||||
|
|
||||||
a) Если bit == 1:
|
|
||||||
// Литерал - копировать один байт
|
|
||||||
byte = *input++
|
|
||||||
window[position] = byte
|
|
||||||
*output++ = byte
|
|
||||||
position = (position + 1) & 0xFFF
|
|
||||||
|
|
||||||
b) Если bit == 0:
|
|
||||||
// LZ77 копирование (2 байта)
|
|
||||||
word = *(uint16*)input
|
|
||||||
input += 2
|
|
||||||
|
|
||||||
b0 = word & 0xFF
|
|
||||||
b1 = (word >> 8) & 0xFF
|
|
||||||
|
|
||||||
offset = b0 | ((b1 & 0xF0) << 4) // 12 бит offset
|
|
||||||
length = (b1 & 0x0F) + 3 // 4 бита длины + 3
|
|
||||||
|
|
||||||
src_pos = offset
|
|
||||||
Повторить length раз:
|
|
||||||
byte = window[src_pos]
|
|
||||||
window[position] = byte
|
|
||||||
*output++ = byte
|
|
||||||
src_pos = (src_pos + 1) & 0xFFF
|
|
||||||
position = (position + 1) & 0xFFF
|
|
||||||
```
|
|
||||||
|
|
||||||
### Формат сжатых данных (простой режим)
|
|
||||||
|
|
||||||
```
|
|
||||||
Битовый поток:
|
|
||||||
|
|
||||||
Битовый поток:
|
|
||||||
|
|
||||||
[FLAG_BIT] [DATA]
|
|
||||||
|
|
||||||
Где:
|
|
||||||
FLAG_BIT = 1: → Литерал (1 байт следует)
|
|
||||||
FLAG_BIT = 0: → LZ77 копирование (2 байта следуют)
|
|
||||||
|
|
||||||
Формат LZ77 копирования (2 байта, little-endian):
|
|
||||||
Байт 0: offset_low (биты 0-7)
|
|
||||||
Байт 1: [length:4][offset_high:4]
|
|
||||||
|
|
||||||
offset = byte0 | ((byte1 & 0xF0) << 4) // 12 бит
|
|
||||||
length = (byte1 & 0x0F) + 3 // 4 бита + 3 = 3-18 байт
|
|
||||||
```
|
|
||||||
|
|
||||||
## Режим 2: Adaptive Huffman режим (a1 < 0)
|
|
||||||
|
|
||||||
Более сложный режим с динамическим Huffman деревом.
|
|
||||||
|
|
||||||
### Инициализация Huffman
|
|
||||||
|
|
||||||
```c
|
|
||||||
Инициализация таблиц:
|
|
||||||
1. Создание таблицы быстрого декодирования (dword_1003B94C[256])
|
|
||||||
2. Инициализация длин кодов (byte_1003BD4C[256])
|
|
||||||
3. Построение начального дерева (627 узлов, T = 2*N_CHAR - 1)
|
|
||||||
где N_CHAR = 314 (256 литералов + 58 кодов длины)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Алгоритм декодирования
|
|
||||||
|
|
||||||
```
|
|
||||||
Инициализация:
|
|
||||||
position = 4078
|
|
||||||
bit_buffer = 0
|
|
||||||
bit_count = 8
|
|
||||||
|
|
||||||
Инициализировать окно значением 0x20 (пробел):
|
|
||||||
for i in range(2039):
|
|
||||||
window[i] = 0x20
|
|
||||||
|
|
||||||
Цикл декомпрессии:
|
|
||||||
Пока не конец выходного буфера:
|
|
||||||
|
|
||||||
1. Декодировать символ через Huffman дерево:
|
|
||||||
|
|
||||||
tree_index = dword_1003E098 // начальный узел
|
|
||||||
|
|
||||||
Пока tree_index < 627: // внутренний узел
|
|
||||||
bit = прочитать_бит()
|
|
||||||
tree_index = tree[tree_index + bit]
|
|
||||||
|
|
||||||
symbol = tree_index - 627 // лист дерева
|
|
||||||
|
|
||||||
Обновить дерево (sub_1001B0AE)
|
|
||||||
|
|
||||||
2. Обработать символ:
|
|
||||||
|
|
||||||
if (symbol < 256):
|
|
||||||
// Литерал
|
|
||||||
window[position] = symbol
|
|
||||||
*output++ = symbol
|
|
||||||
position = (position + 1) & 0xFFF
|
|
||||||
|
|
||||||
else:
|
|
||||||
// LZSS копирование (LZHUF)
|
|
||||||
length = symbol - 253 // 3..60
|
|
||||||
match_pos = decode_position() // префикс + 6 бит
|
|
||||||
|
|
||||||
src_pos = (position - 1 - match_pos) & 0xFFF
|
|
||||||
|
|
||||||
Повторить length раз:
|
|
||||||
byte = window[src_pos]
|
|
||||||
window[position] = byte
|
|
||||||
*output++ = byte
|
|
||||||
src_pos = (src_pos + 1) & 0xFFF
|
|
||||||
position = (position + 1) & 0xFFF
|
|
||||||
```
|
|
||||||
|
|
||||||
### Обновление дерева
|
|
||||||
|
|
||||||
Адаптивное Huffman дерево обновляется после каждого декодированного символа:
|
|
||||||
|
|
||||||
```
|
|
||||||
Алгоритм обновления:
|
|
||||||
1. Увеличить счетчик частоты символа
|
|
||||||
2. Если частота превысила порог:
|
|
||||||
Перестроить узлы дерева (swapping)
|
|
||||||
3. Если счетчик достиг 0x8000:
|
|
||||||
Пересчитать все частоты (разделить на 2)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Псевдокод полной реализации
|
|
||||||
|
|
||||||
### Декодер (простой режим)
|
|
||||||
|
|
||||||
```python
|
|
||||||
def fres_decompress_simple(input_data, output_size):
|
|
||||||
"""
|
|
||||||
FRES декомпрессия в простом режиме
|
|
||||||
"""
|
|
||||||
# Инициализация
|
|
||||||
window = bytearray(4096)
|
|
||||||
position = 4036
|
|
||||||
output = bytearray()
|
|
||||||
|
|
||||||
input_pos = 0
|
|
||||||
flags = 0
|
|
||||||
flag_bits = 0
|
|
||||||
|
|
||||||
while len(output) < output_size and input_pos < len(input_data):
|
|
||||||
# Читаем флаг (LSB-first)
|
|
||||||
if flag_bits == 0:
|
|
||||||
if input_pos >= len(input_data):
|
|
||||||
break
|
|
||||||
flags = input_data[input_pos]
|
|
||||||
input_pos += 1
|
|
||||||
flag_bits = 8
|
|
||||||
|
|
||||||
flag = flags & 1
|
|
||||||
flags >>= 1
|
|
||||||
flag_bits -= 1
|
|
||||||
|
|
||||||
# Обработка по флагу
|
|
||||||
if flag: # 1 = literal
|
|
||||||
# Литерал
|
|
||||||
if input_pos >= len(input_data):
|
|
||||||
break
|
|
||||||
byte = input_data[input_pos]
|
|
||||||
input_pos += 1
|
|
||||||
|
|
||||||
window[position] = byte
|
|
||||||
output.append(byte)
|
|
||||||
position = (position + 1) & 0xFFF
|
|
||||||
else: # 0 = backref (2 байта)
|
|
||||||
if input_pos + 1 >= len(input_data):
|
|
||||||
break
|
|
||||||
|
|
||||||
b0 = input_data[input_pos]
|
|
||||||
b1 = input_data[input_pos + 1]
|
|
||||||
input_pos += 2
|
|
||||||
|
|
||||||
offset = b0 | ((b1 & 0xF0) << 4)
|
|
||||||
length = (b1 & 0x0F) + 3
|
|
||||||
|
|
||||||
for _ in range(length):
|
|
||||||
if len(output) >= output_size:
|
|
||||||
break
|
|
||||||
|
|
||||||
byte = window[offset]
|
|
||||||
window[position] = byte
|
|
||||||
output.append(byte)
|
|
||||||
|
|
||||||
offset = (offset + 1) & 0xFFF
|
|
||||||
position = (position + 1) & 0xFFF
|
|
||||||
|
|
||||||
return bytes(output[:output_size])
|
|
||||||
```
|
|
||||||
|
|
||||||
### Вспомогательные функции
|
|
||||||
|
|
||||||
```python
|
|
||||||
class BitReader:
|
|
||||||
"""Класс для побитового чтения"""
|
|
||||||
|
|
||||||
def __init__(self, data):
|
|
||||||
self.data = data
|
|
||||||
self.pos = 0
|
|
||||||
self.bit_buffer = 0
|
|
||||||
self.bits_available = 0
|
|
||||||
|
|
||||||
def read_bit(self):
|
|
||||||
"""Прочитать один бит"""
|
|
||||||
if self.bits_available == 0:
|
|
||||||
if self.pos >= len(self.data):
|
|
||||||
return 0
|
|
||||||
self.bit_buffer = self.data[self.pos]
|
|
||||||
self.pos += 1
|
|
||||||
self.bits_available = 8
|
|
||||||
|
|
||||||
bit = self.bit_buffer & 1
|
|
||||||
self.bit_buffer >>= 1
|
|
||||||
self.bits_available -= 1
|
|
||||||
return bit
|
|
||||||
|
|
||||||
def read_bits(self, count):
|
|
||||||
"""Прочитать несколько бит"""
|
|
||||||
result = 0
|
|
||||||
for i in range(count):
|
|
||||||
result |= self.read_bit() << i
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
def initialize_window():
|
|
||||||
"""Инициализация окна для Huffman режима"""
|
|
||||||
window = bytearray(4096)
|
|
||||||
# Заполняем начальным значением
|
|
||||||
for i in range(4078):
|
|
||||||
window[i] = 0x20 # Пробел
|
|
||||||
return window
|
|
||||||
```
|
|
||||||
|
|
||||||
## Ключевые особенности
|
|
||||||
|
|
||||||
### 1. Циклический буфер
|
|
||||||
|
|
||||||
- Размер: 4096 байт (12 бит адресации)
|
|
||||||
- Маска: `0xFFF` для циклического доступа
|
|
||||||
- Начальная позиция зависит от режима
|
|
||||||
|
|
||||||
### 2. Dual-режимы
|
|
||||||
|
|
||||||
- **Простой**: Быстрее, меньше сжатие, для данных с низкой энтропией
|
|
||||||
- **Huffman**: Медленнее, лучше сжатие, для данных с высокой энтропией
|
|
||||||
|
|
||||||
### 3. LZ77 кодирование
|
|
||||||
|
|
||||||
- Offset: 12 бит (0-4095)
|
|
||||||
- Length: 4 бита + 3 (3-18 байт)
|
|
||||||
- Максимальное копирование: 18 байт
|
|
||||||
|
|
||||||
### 4. Битовые флаги
|
|
||||||
|
|
||||||
Используется один флаговый бит (LSB-first) для определения типа данных:
|
|
||||||
|
|
||||||
- `1` → literal (1 байт)
|
|
||||||
- `0` → backref (2 байта)
|
|
||||||
|
|
||||||
## Проблемы реализации
|
|
||||||
|
|
||||||
### 1. Битовый порядок
|
|
||||||
|
|
||||||
Биты читаются справа налево (LSB first), что может вызвать путаницу
|
|
||||||
|
|
||||||
### 2. Huffman дерево
|
|
||||||
|
|
||||||
Адаптивное дерево требует точного отслеживания частот и правильной перестройки
|
|
||||||
|
|
||||||
### 3. Граничные условия
|
|
||||||
|
|
||||||
Необходимо тщательно проверять границы буферов
|
|
||||||
|
|
||||||
- В простом режиме перед backref нужно гарантировать наличие **2 байт** входных данных
|
|
||||||
|
|
||||||
## Примеры данных
|
|
||||||
|
|
||||||
### Пример 1: Литералы (простой режим)
|
|
||||||
|
|
||||||
```
|
|
||||||
Входные биты: 00 00 00 ...
|
|
||||||
Выход: Последовательность литералов
|
|
||||||
|
|
||||||
Пример:
|
|
||||||
Flags: 0xFF (11111111)
|
|
||||||
Data: 0x41 ('A'), 0x42 ('B'), 0x43 ('C'), ...
|
|
||||||
Выход: "ABC..."
|
|
||||||
```
|
|
||||||
|
|
||||||
### Пример 2: LZ77 копирование
|
|
||||||
|
|
||||||
```
|
|
||||||
Входные биты: 10 ...
|
|
||||||
Выход: Копирование из окна
|
|
||||||
|
|
||||||
Пример:
|
|
||||||
Flags: 0x00 (00000000) - первый бит = 0
|
|
||||||
Bytes: b0=0x34, b1=0x12
|
|
||||||
|
|
||||||
Разбор:
|
|
||||||
offset = 0x34 | ((0x12 & 0xF0) << 4) = 0x234
|
|
||||||
length = (0x12 & 0x0F) + 3 = 5
|
|
||||||
|
|
||||||
Действие: Скопировать 5 байт с позиции offset
|
|
||||||
```
|
|
||||||
|
|
||||||
## Отладка
|
|
||||||
|
|
||||||
Для отладки рекомендуется:
|
|
||||||
|
|
||||||
```python
|
|
||||||
def debug_fres_decompress(input_data, output_size):
|
|
||||||
"""Версия с отладочным выводом"""
|
|
||||||
print(f"Input size: {len(input_data)}")
|
|
||||||
print(f"Output size: {output_size}")
|
|
||||||
|
|
||||||
# ... реализация с print на каждом шаге
|
|
||||||
|
|
||||||
print(f"Flag: {flag}")
|
|
||||||
if is_literal:
|
|
||||||
print(f" Literal: 0x{byte:02X}")
|
|
||||||
else:
|
|
||||||
print(f" LZ77: offset={offset}, length={length}")
|
|
||||||
```
|
|
||||||
|
|
||||||
## Заключение
|
|
||||||
|
|
||||||
FRES — это эффективный гибридный алгоритм, сочетающий:
|
|
||||||
|
|
||||||
- RLE для повторяющихся данных
|
|
||||||
- LZ77 для ссылок на предыдущие данные
|
|
||||||
- Опциональный Huffman для символов
|
|
||||||
|
|
||||||
**Сложность декомпрессии:** O(n) где n — размер выходных данных
|
|
||||||
|
|
||||||
**Размер окна:** 4 КБ — хороший баланс между памятью и степенью сжатия
|
|
||||||
@@ -1,605 +0,0 @@
|
|||||||
# Huffman Декомпрессия
|
|
||||||
|
|
||||||
## Обзор
|
|
||||||
|
|
||||||
Это реализация **RAW-DEFLATE (inflate)**, используемого в [NRes](overview.md). Поток подаётся без zlib-обёртки (нет 2-байтового заголовка и Adler32). Алгоритм поддерживает три режима блоков и использует два Huffman дерева для кодирования литералов/длин и расстояний.
|
|
||||||
|
|
||||||
```c
|
|
||||||
int __thiscall sub_1001AF10(
|
|
||||||
unsigned int *this, // Контекст декодера (HuffmanContext)
|
|
||||||
int *a2 // Выходной параметр (результат операции)
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Структура контекста (HuffmanContext)
|
|
||||||
|
|
||||||
```c
|
|
||||||
struct HuffmanContext {
|
|
||||||
uint8_t window[0x10000]; // 0x00000-0x0FFFF: Внутренний буфер/окно
|
|
||||||
uint32_t compressedSize; // 0x10000: packedSize
|
|
||||||
uint32_t outputPosition; // 0x10004: Сколько уже выведено
|
|
||||||
uint32_t windowPos; // 0x10008: Позиция в 0x8000 окне
|
|
||||||
uint32_t sourcePtr; // 0x1000C: Указатель на сжатые данные
|
|
||||||
uint32_t destPtr; // 0x10010: Указатель на выходной буфер
|
|
||||||
uint32_t sourcePos; // 0x10014: Текущая позиция чтения
|
|
||||||
uint32_t unpackedSize; // 0x10018: Ожидаемый размер распаковки
|
|
||||||
uint32_t bitBufferValue; // 0x1001C: Битовый буфер
|
|
||||||
uint32_t bitsAvailable; // 0x10020: Количество доступных бит
|
|
||||||
uint32_t maxWindowPosSeen; // 0x10024: Максимум окна (статистика)
|
|
||||||
// ...
|
|
||||||
};
|
|
||||||
|
|
||||||
// Смещения в структуре (индексация this[]):
|
|
||||||
#define CTX_COMPRESSED_SIZE 0x4000 // this[0x4000] == 0x10000
|
|
||||||
#define CTX_OUTPUT_POS 16385 // this[16385] == 0x10004
|
|
||||||
#define CTX_WINDOW_POS 16386 // this[16386] == 0x10008
|
|
||||||
#define CTX_SOURCE_PTR 16387 // this[16387] == 0x1000C
|
|
||||||
#define CTX_DEST_PTR 16388 // this[16388] == 0x10010
|
|
||||||
#define CTX_SOURCE_POS 16389 // this[16389] == 0x10014
|
|
||||||
#define CTX_UNPACKED_SIZE 16390 // this[16390] == 0x10018
|
|
||||||
#define CTX_BIT_BUFFER 16391 // this[16391] == 0x1001C
|
|
||||||
#define CTX_BITS_COUNT 16392 // this[16392] == 0x10020
|
|
||||||
#define CTX_MAX_WINDOW_POS 16393 // this[16393] == 0x10024
|
|
||||||
```
|
|
||||||
|
|
||||||
## Три режима блоков
|
|
||||||
|
|
||||||
Алгоритм определяет тип блока по первым 3 битам:
|
|
||||||
|
|
||||||
```
|
|
||||||
Биты: [TYPE:2] [FINAL:1]
|
|
||||||
|
|
||||||
FINAL = 1: Это последний блок
|
|
||||||
TYPE:
|
|
||||||
00 = Несжатый блок (сырые данные)
|
|
||||||
01 = Сжатый с фиксированными Huffman кодами
|
|
||||||
10 = Сжатый с динамическими Huffman кодами
|
|
||||||
11 = Зарезервировано (ошибка)
|
|
||||||
```
|
|
||||||
|
|
||||||
Соответствие функциям:
|
|
||||||
|
|
||||||
- type 0 → `sub_1001A750` (stored)
|
|
||||||
- type 1 → `sub_1001A8C0` (fixed Huffman)
|
|
||||||
- type 2 → `sub_1001AA30` (dynamic Huffman)
|
|
||||||
|
|
||||||
### Основной цикл декодирования
|
|
||||||
|
|
||||||
```c
|
|
||||||
int decode_block(HuffmanContext* ctx) {
|
|
||||||
// Читаем первый бит (FINAL)
|
|
||||||
int final_bit = read_bit(ctx);
|
|
||||||
|
|
||||||
// Читаем 2 бита (TYPE)
|
|
||||||
int type = read_bits(ctx, 2);
|
|
||||||
|
|
||||||
switch (type) {
|
|
||||||
case 0: // 00 - Несжатый блок
|
|
||||||
return decode_uncompressed_block(ctx);
|
|
||||||
|
|
||||||
case 1: // 01 - Фиксированные Huffman коды
|
|
||||||
return decode_fixed_huffman_block(ctx);
|
|
||||||
|
|
||||||
case 2: // 10 - Динамические Huffman коды
|
|
||||||
return decode_dynamic_huffman_block(ctx);
|
|
||||||
|
|
||||||
case 3: // 11 - Ошибка
|
|
||||||
return 2; // Неподдерживаемый тип
|
|
||||||
}
|
|
||||||
|
|
||||||
return final_bit ? 0 : 1; // 0 = конец, 1 = есть еще блоки
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Режим 0: Несжатый блок
|
|
||||||
|
|
||||||
Простое копирование байтов без сжатия.
|
|
||||||
|
|
||||||
### Алгоритм
|
|
||||||
|
|
||||||
```python
|
|
||||||
def decode_uncompressed_block(ctx):
|
|
||||||
"""
|
|
||||||
Формат несжатого блока:
|
|
||||||
[LEN:16][NLEN:16][DATA:LEN]
|
|
||||||
|
|
||||||
Где:
|
|
||||||
LEN - длина данных (little-endian)
|
|
||||||
NLEN - инверсия LEN (~LEN)
|
|
||||||
DATA - сырые данные
|
|
||||||
"""
|
|
||||||
# Выравнивание к границе байта
|
|
||||||
bits_to_skip = ctx.bits_available & 7
|
|
||||||
ctx.bit_buffer >>= bits_to_skip
|
|
||||||
ctx.bits_available -= bits_to_skip
|
|
||||||
|
|
||||||
# Читаем длину (16 бит)
|
|
||||||
length = read_bits(ctx, 16)
|
|
||||||
|
|
||||||
# Читаем инверсию длины (16 бит)
|
|
||||||
nlength = read_bits(ctx, 16)
|
|
||||||
|
|
||||||
# Проверка целостности
|
|
||||||
if length != (~nlength & 0xFFFF):
|
|
||||||
return 1 # Ошибка
|
|
||||||
|
|
||||||
# Копируем данные
|
|
||||||
for i in range(length):
|
|
||||||
byte = read_byte(ctx)
|
|
||||||
write_output_byte(ctx, byte)
|
|
||||||
|
|
||||||
# Проверка переполнения выходного буфера
|
|
||||||
if ctx.output_position >= 0x8000:
|
|
||||||
flush_output_buffer(ctx)
|
|
||||||
|
|
||||||
return 0
|
|
||||||
```
|
|
||||||
|
|
||||||
### Детали
|
|
||||||
|
|
||||||
- Данные копируются "как есть"
|
|
||||||
- Используется для несжимаемых данных
|
|
||||||
- Требует выравнивания по байтам перед чтением длины
|
|
||||||
|
|
||||||
## Режим 1: Фиксированные Huffman коды
|
|
||||||
|
|
||||||
Использует предопределенные Huffman таблицы.
|
|
||||||
|
|
||||||
### Фиксированные таблицы длин кодов
|
|
||||||
|
|
||||||
```python
|
|
||||||
# Таблица для литералов/длин (288 символов)
|
|
||||||
FIXED_LITERAL_LENGTHS = [
|
|
||||||
8, 8, 8, 8, ..., 8, # 0-143: коды длины 8 (144 символа)
|
|
||||||
9, 9, 9, 9, ..., 9, # 144-255: коды длины 9 (112 символов)
|
|
||||||
7, 7, 7, 7, ..., 7, # 256-279: коды длины 7 (24 символа)
|
|
||||||
8, 8, 8, 8, ..., 8 # 280-287: коды длины 8 (8 символов)
|
|
||||||
]
|
|
||||||
|
|
||||||
# Таблица для расстояний (30 символов)
|
|
||||||
FIXED_DISTANCE_LENGTHS = [
|
|
||||||
5, 5, 5, 5, ..., 5 # 0-29: все коды длины 5
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
### Алгоритм декодирования
|
|
||||||
|
|
||||||
```python
|
|
||||||
def decode_fixed_huffman_block(ctx):
|
|
||||||
"""Декодирование блока с фиксированными Huffman кодами"""
|
|
||||||
|
|
||||||
# Инициализация фиксированных таблиц
|
|
||||||
lit_tree = build_huffman_tree(FIXED_LITERAL_LENGTHS)
|
|
||||||
dist_tree = build_huffman_tree(FIXED_DISTANCE_LENGTHS)
|
|
||||||
|
|
||||||
while True:
|
|
||||||
# Декодировать символ литерала/длины
|
|
||||||
symbol = decode_huffman_symbol(ctx, lit_tree)
|
|
||||||
|
|
||||||
if symbol < 256:
|
|
||||||
# Литерал - просто вывести байт
|
|
||||||
write_output_byte(ctx, symbol)
|
|
||||||
|
|
||||||
elif symbol == 256:
|
|
||||||
# Конец блока
|
|
||||||
break
|
|
||||||
|
|
||||||
else:
|
|
||||||
# Символ длины (257-285)
|
|
||||||
length = decode_length(ctx, symbol)
|
|
||||||
|
|
||||||
# Декодировать расстояние
|
|
||||||
dist_symbol = decode_huffman_symbol(ctx, dist_tree)
|
|
||||||
distance = decode_distance(ctx, dist_symbol)
|
|
||||||
|
|
||||||
# Скопировать из истории
|
|
||||||
copy_from_history(ctx, distance, length)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Таблицы экстра-бит
|
|
||||||
|
|
||||||
```python
|
|
||||||
# Дополнительные биты для длины
|
|
||||||
LENGTH_EXTRA_BITS = {
|
|
||||||
257: 0, 258: 0, 259: 0, 260: 0, 261: 0, 262: 0, 263: 0, 264: 0, # 3-10
|
|
||||||
265: 1, 266: 1, 267: 1, 268: 1, # 11-18
|
|
||||||
269: 2, 270: 2, 271: 2, 272: 2, # 19-34
|
|
||||||
273: 3, 274: 3, 275: 3, 276: 3, # 35-66
|
|
||||||
277: 4, 278: 4, 279: 4, 280: 4, # 67-130
|
|
||||||
281: 5, 282: 5, 283: 5, 284: 5, # 131-257
|
|
||||||
285: 0 # 258
|
|
||||||
}
|
|
||||||
|
|
||||||
LENGTH_BASE = {
|
|
||||||
257: 3, 258: 4, 259: 5, ..., 285: 258
|
|
||||||
}
|
|
||||||
|
|
||||||
# Дополнительные биты для расстояния
|
|
||||||
DISTANCE_EXTRA_BITS = {
|
|
||||||
0: 0, 1: 0, 2: 0, 3: 0, # 1-4
|
|
||||||
4: 1, 5: 1, 6: 2, 7: 2, # 5-12
|
|
||||||
8: 3, 9: 3, 10: 4, 11: 4, # 13-48
|
|
||||||
12: 5, 13: 5, 14: 6, 15: 6, # 49-192
|
|
||||||
16: 7, 17: 7, 18: 8, 19: 8, # 193-768
|
|
||||||
20: 9, 21: 9, 22: 10, 23: 10, # 769-3072
|
|
||||||
24: 11, 25: 11, 26: 12, 27: 12, # 3073-12288
|
|
||||||
28: 13, 29: 13 # 12289-24576
|
|
||||||
}
|
|
||||||
|
|
||||||
DISTANCE_BASE = {
|
|
||||||
0: 1, 1: 2, 2: 3, 3: 4, ..., 29: 24577
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Декодирование длины и расстояния
|
|
||||||
|
|
||||||
```python
|
|
||||||
def decode_length(ctx, symbol):
|
|
||||||
"""Декодировать длину из символа"""
|
|
||||||
base = LENGTH_BASE[symbol]
|
|
||||||
extra_bits = LENGTH_EXTRA_BITS[symbol]
|
|
||||||
|
|
||||||
if extra_bits > 0:
|
|
||||||
extra = read_bits(ctx, extra_bits)
|
|
||||||
return base + extra
|
|
||||||
|
|
||||||
return base
|
|
||||||
|
|
||||||
|
|
||||||
def decode_distance(ctx, symbol):
|
|
||||||
"""Декодировать расстояние из символа"""
|
|
||||||
base = DISTANCE_BASE[symbol]
|
|
||||||
extra_bits = DISTANCE_EXTRA_BITS[symbol]
|
|
||||||
|
|
||||||
if extra_bits > 0:
|
|
||||||
extra = read_bits(ctx, extra_bits)
|
|
||||||
return base + extra
|
|
||||||
|
|
||||||
return base
|
|
||||||
```
|
|
||||||
|
|
||||||
## Режим 2: Динамические Huffman коды
|
|
||||||
|
|
||||||
Самый сложный режим. Huffman таблицы передаются в начале блока.
|
|
||||||
|
|
||||||
### Формат заголовка динамического блока
|
|
||||||
|
|
||||||
```
|
|
||||||
Биты заголовка:
|
|
||||||
[HLIT:5] - Количество литерал/длина кодов - 257 (значение: 257-286)
|
|
||||||
[HDIST:5] - Количество расстояние кодов - 1 (значение: 1-30)
|
|
||||||
[HCLEN:4] - Количество длин кодов для code length алфавита - 4 (значение: 4-19)
|
|
||||||
|
|
||||||
Далее идут длины кодов для code length алфавита:
|
|
||||||
[CL0:3] [CL1:3] ... [CL(HCLEN-1):3]
|
|
||||||
|
|
||||||
Порядок code length кодов:
|
|
||||||
16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15
|
|
||||||
```
|
|
||||||
|
|
||||||
### Алгоритм декодирования
|
|
||||||
|
|
||||||
```python
|
|
||||||
def decode_dynamic_huffman_block(ctx):
|
|
||||||
"""Декодирование блока с динамическими Huffman кодами"""
|
|
||||||
|
|
||||||
# 1. Читаем заголовок
|
|
||||||
hlit = read_bits(ctx, 5) + 257 # Количество литерал/длина кодов
|
|
||||||
hdist = read_bits(ctx, 5) + 1 # Количество расстояние кодов
|
|
||||||
hclen = read_bits(ctx, 4) + 4 # Количество code length кодов
|
|
||||||
|
|
||||||
if hlit > 286 or hdist > 30:
|
|
||||||
return 1 # Ошибка
|
|
||||||
|
|
||||||
# 2. Читаем длины для code length алфавита
|
|
||||||
CODE_LENGTH_ORDER = [16, 17, 18, 0, 8, 7, 9, 6, 10, 5,
|
|
||||||
11, 4, 12, 3, 13, 2, 14, 1, 15]
|
|
||||||
|
|
||||||
code_length_lengths = [0] * 19
|
|
||||||
for i in range(hclen):
|
|
||||||
code_length_lengths[CODE_LENGTH_ORDER[i]] = read_bits(ctx, 3)
|
|
||||||
|
|
||||||
# 3. Строим дерево для code length
|
|
||||||
cl_tree = build_huffman_tree(code_length_lengths)
|
|
||||||
|
|
||||||
# 4. Декодируем длины литерал/длина и расстояние кодов
|
|
||||||
lengths = decode_code_lengths(ctx, cl_tree, hlit + hdist)
|
|
||||||
|
|
||||||
# 5. Разделяем на два алфавита
|
|
||||||
literal_lengths = lengths[:hlit]
|
|
||||||
distance_lengths = lengths[hlit:]
|
|
||||||
|
|
||||||
# 6. Строим деревья для декодирования
|
|
||||||
lit_tree = build_huffman_tree(literal_lengths)
|
|
||||||
dist_tree = build_huffman_tree(distance_lengths)
|
|
||||||
|
|
||||||
# 7. Декодируем данные (аналогично фиксированному режиму)
|
|
||||||
return decode_huffman_data(ctx, lit_tree, dist_tree)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Декодирование длин кодов
|
|
||||||
|
|
||||||
Используется специальный алфавит с тремя специальными символами:
|
|
||||||
|
|
||||||
```python
|
|
||||||
def decode_code_lengths(ctx, cl_tree, total_count):
|
|
||||||
"""
|
|
||||||
Декодирование последовательности длин кодов
|
|
||||||
|
|
||||||
Специальные символы:
|
|
||||||
16 - Повторить предыдущую длину 3-6 раз (2 доп. бита)
|
|
||||||
17 - Повторить 0 длину 3-10 раз (3 доп. бита)
|
|
||||||
18 - Повторить 0 длину 11-138 раз (7 доп. бит)
|
|
||||||
"""
|
|
||||||
lengths = []
|
|
||||||
last_length = 0
|
|
||||||
|
|
||||||
while len(lengths) < total_count:
|
|
||||||
symbol = decode_huffman_symbol(ctx, cl_tree)
|
|
||||||
|
|
||||||
if symbol < 16:
|
|
||||||
# Обычная длина (0-15)
|
|
||||||
lengths.append(symbol)
|
|
||||||
last_length = symbol
|
|
||||||
|
|
||||||
elif symbol == 16:
|
|
||||||
# Повторить предыдущую длину
|
|
||||||
repeat = read_bits(ctx, 2) + 3
|
|
||||||
lengths.extend([last_length] * repeat)
|
|
||||||
|
|
||||||
elif symbol == 17:
|
|
||||||
# Повторить ноль (короткий)
|
|
||||||
repeat = read_bits(ctx, 3) + 3
|
|
||||||
lengths.extend([0] * repeat)
|
|
||||||
last_length = 0
|
|
||||||
|
|
||||||
elif symbol == 18:
|
|
||||||
# Повторить ноль (длинный)
|
|
||||||
repeat = read_bits(ctx, 7) + 11
|
|
||||||
lengths.extend([0] * repeat)
|
|
||||||
last_length = 0
|
|
||||||
|
|
||||||
return lengths[:total_count]
|
|
||||||
```
|
|
||||||
|
|
||||||
## Построение Huffman дерева
|
|
||||||
|
|
||||||
```python
|
|
||||||
def build_huffman_tree(code_lengths):
|
|
||||||
"""
|
|
||||||
Построить Huffman дерево из длин кодов
|
|
||||||
|
|
||||||
Использует алгоритм "canonical Huffman codes"
|
|
||||||
"""
|
|
||||||
max_length = max(code_lengths) if code_lengths else 0
|
|
||||||
|
|
||||||
# 1. Подсчитать количество кодов каждой длины
|
|
||||||
bl_count = [0] * (max_length + 1)
|
|
||||||
for length in code_lengths:
|
|
||||||
if length > 0:
|
|
||||||
bl_count[length] += 1
|
|
||||||
|
|
||||||
# 2. Вычислить первый код для каждой длины
|
|
||||||
code = 0
|
|
||||||
next_code = [0] * (max_length + 1)
|
|
||||||
|
|
||||||
for bits in range(1, max_length + 1):
|
|
||||||
code = (code + bl_count[bits - 1]) << 1
|
|
||||||
next_code[bits] = code
|
|
||||||
|
|
||||||
# 3. Присвоить числовые коды символам
|
|
||||||
tree = {}
|
|
||||||
for symbol, length in enumerate(code_lengths):
|
|
||||||
if length > 0:
|
|
||||||
tree[symbol] = {
|
|
||||||
'code': next_code[length],
|
|
||||||
'length': length
|
|
||||||
}
|
|
||||||
next_code[length] += 1
|
|
||||||
|
|
||||||
# 4. Создать структуру быстрого поиска
|
|
||||||
lookup_table = create_lookup_table(tree)
|
|
||||||
|
|
||||||
return lookup_table
|
|
||||||
|
|
||||||
|
|
||||||
def decode_huffman_symbol(ctx, tree):
|
|
||||||
"""Декодировать один символ из Huffman дерева"""
|
|
||||||
code = 0
|
|
||||||
length = 0
|
|
||||||
|
|
||||||
for length in range(1, 16):
|
|
||||||
bit = read_bit(ctx)
|
|
||||||
code = (code << 1) | bit
|
|
||||||
|
|
||||||
# Проверить в таблице быстрого поиска
|
|
||||||
if (code, length) in tree:
|
|
||||||
return tree[(code, length)]
|
|
||||||
|
|
||||||
return -1 # Ошибка декодирования
|
|
||||||
```
|
|
||||||
|
|
||||||
## Управление выходным буфером
|
|
||||||
|
|
||||||
```python
|
|
||||||
def write_output_byte(ctx, byte):
|
|
||||||
"""Записать байт в выходной буфер"""
|
|
||||||
# Записываем в окно 0x8000
|
|
||||||
ctx.window[ctx.windowPos] = byte
|
|
||||||
ctx.windowPos += 1
|
|
||||||
|
|
||||||
# Если окно заполнено (32KB)
|
|
||||||
if ctx.windowPos >= 0x8000:
|
|
||||||
flush_output_buffer(ctx)
|
|
||||||
|
|
||||||
|
|
||||||
def flush_output_buffer(ctx):
|
|
||||||
"""Сбросить выходной буфер в финальный выход"""
|
|
||||||
# Копируем окно в финальный выходной буфер
|
|
||||||
dest_offset = ctx.outputPosition + ctx.destPtr
|
|
||||||
memcpy(dest_offset, ctx.window, ctx.windowPos)
|
|
||||||
|
|
||||||
# Обновляем счетчики
|
|
||||||
ctx.outputPosition += ctx.windowPos
|
|
||||||
ctx.windowPos = 0
|
|
||||||
|
|
||||||
|
|
||||||
def copy_from_history(ctx, distance, length):
|
|
||||||
"""Скопировать данные из истории (LZ77)"""
|
|
||||||
# Позиция источника в циклическом буфере
|
|
||||||
src_pos = (ctx.windowPos - distance) & 0x7FFF
|
|
||||||
|
|
||||||
for i in range(length):
|
|
||||||
byte = ctx.window[src_pos]
|
|
||||||
write_output_byte(ctx, byte)
|
|
||||||
src_pos = (src_pos + 1) & 0x7FFF
|
|
||||||
```
|
|
||||||
|
|
||||||
## Полная реализация на Python
|
|
||||||
|
|
||||||
```python
|
|
||||||
class HuffmanDecoder:
|
|
||||||
"""Полный RAW-DEFLATE декодер"""
|
|
||||||
|
|
||||||
def __init__(self, input_data, output_size):
|
|
||||||
self.input_data = input_data
|
|
||||||
self.output_size = output_size
|
|
||||||
self.input_pos = 0
|
|
||||||
self.bit_buffer = 0
|
|
||||||
self.bits_available = 0
|
|
||||||
self.output = bytearray()
|
|
||||||
self.history = bytearray(32768) # 32KB циклический буфер
|
|
||||||
self.history_pos = 0
|
|
||||||
|
|
||||||
def read_bit(self):
|
|
||||||
"""Прочитать один бит"""
|
|
||||||
if self.bits_available == 0:
|
|
||||||
if self.input_pos >= len(self.input_data):
|
|
||||||
return 0
|
|
||||||
self.bit_buffer = self.input_data[self.input_pos]
|
|
||||||
self.input_pos += 1
|
|
||||||
self.bits_available = 8
|
|
||||||
|
|
||||||
bit = self.bit_buffer & 1
|
|
||||||
self.bit_buffer >>= 1
|
|
||||||
self.bits_available -= 1
|
|
||||||
return bit
|
|
||||||
|
|
||||||
def read_bits(self, count):
|
|
||||||
"""Прочитать несколько бит (LSB first)"""
|
|
||||||
result = 0
|
|
||||||
for i in range(count):
|
|
||||||
result |= self.read_bit() << i
|
|
||||||
return result
|
|
||||||
|
|
||||||
def write_byte(self, byte):
|
|
||||||
"""Записать байт в выход и историю"""
|
|
||||||
self.output.append(byte)
|
|
||||||
self.history[self.history_pos] = byte
|
|
||||||
self.history_pos = (self.history_pos + 1) & 0x7FFF
|
|
||||||
|
|
||||||
def copy_from_history(self, distance, length):
|
|
||||||
"""Скопировать из истории"""
|
|
||||||
src_pos = (self.history_pos - distance) & 0x7FFF
|
|
||||||
|
|
||||||
for _ in range(length):
|
|
||||||
byte = self.history[src_pos]
|
|
||||||
self.write_byte(byte)
|
|
||||||
src_pos = (src_pos + 1) & 0x7FFF
|
|
||||||
|
|
||||||
def decompress(self):
|
|
||||||
"""Основной цикл декомпрессии"""
|
|
||||||
while len(self.output) < self.output_size:
|
|
||||||
# Читаем заголовок блока
|
|
||||||
final = self.read_bit()
|
|
||||||
block_type = self.read_bits(2)
|
|
||||||
|
|
||||||
if block_type == 0:
|
|
||||||
# Несжатый блок
|
|
||||||
if not self.decode_uncompressed_block():
|
|
||||||
break
|
|
||||||
elif block_type == 1:
|
|
||||||
# Фиксированные Huffman коды
|
|
||||||
if not self.decode_fixed_huffman_block():
|
|
||||||
break
|
|
||||||
elif block_type == 2:
|
|
||||||
# Динамические Huffman коды
|
|
||||||
if not self.decode_dynamic_huffman_block():
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
# Ошибка
|
|
||||||
raise ValueError("Invalid block type")
|
|
||||||
|
|
||||||
if final:
|
|
||||||
break
|
|
||||||
|
|
||||||
return bytes(self.output[:self.output_size])
|
|
||||||
|
|
||||||
# ... реализации decode_*_block методов ...
|
|
||||||
```
|
|
||||||
|
|
||||||
## Оптимизации
|
|
||||||
|
|
||||||
### 1. Таблица быстрого поиска
|
|
||||||
|
|
||||||
```python
|
|
||||||
# Предвычисленная таблица для 9 бит (первый уровень)
|
|
||||||
FAST_LOOKUP_BITS = 9
|
|
||||||
fast_table = [None] * (1 << FAST_LOOKUP_BITS)
|
|
||||||
|
|
||||||
# Заполнение таблицы при построении дерева
|
|
||||||
for symbol, info in tree.items():
|
|
||||||
if info['length'] <= FAST_LOOKUP_BITS:
|
|
||||||
# Все возможные префиксы для этого кода
|
|
||||||
code = info['code']
|
|
||||||
for i in range(1 << (FAST_LOOKUP_BITS - info['length'])):
|
|
||||||
lookup_code = code | (i << info['length'])
|
|
||||||
fast_table[lookup_code] = symbol
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Буферизация битов
|
|
||||||
|
|
||||||
```python
|
|
||||||
# Читать по 32 бита за раз вместо побитового чтения
|
|
||||||
def refill_bits(self):
|
|
||||||
"""Пополнить битовый буфер"""
|
|
||||||
while self.bits_available < 24 and self.input_pos < len(self.input_data):
|
|
||||||
byte = self.input_data[self.input_pos]
|
|
||||||
self.input_pos += 1
|
|
||||||
self.bit_buffer |= byte << self.bits_available
|
|
||||||
self.bits_available += 8
|
|
||||||
```
|
|
||||||
|
|
||||||
## Отладка и тестирование
|
|
||||||
|
|
||||||
```python
|
|
||||||
def debug_huffman_decode(data):
|
|
||||||
"""Декодирование с отладочной информацией"""
|
|
||||||
decoder = HuffmanDecoder(data, len(data) * 10)
|
|
||||||
|
|
||||||
original_read_bits = decoder.read_bits
|
|
||||||
def debug_read_bits(count):
|
|
||||||
result = original_read_bits(count)
|
|
||||||
print(f"Read {count} bits: 0x{result:0{count//4}X} ({result})")
|
|
||||||
return result
|
|
||||||
|
|
||||||
decoder.read_bits = debug_read_bits
|
|
||||||
return decoder.decompress()
|
|
||||||
```
|
|
||||||
|
|
||||||
## Заключение
|
|
||||||
|
|
||||||
Этот декодер реализует **RAW-DEFLATE** с тремя режимами блоков:
|
|
||||||
|
|
||||||
1. **Несжатый** - для несжимаемых данных
|
|
||||||
2. **Фиксированный Huffman** - быстрое декодирование с предопределенными таблицами
|
|
||||||
3. **Динамический Huffman** - максимальное сжатие с пользовательскими таблицами
|
|
||||||
|
|
||||||
**Ключевые особенности:**
|
|
||||||
|
|
||||||
- Поддержка LZ77 для повторяющихся последовательностей
|
|
||||||
- Канонические Huffman коды для эффективного декодирования
|
|
||||||
- Циклический буфер 32KB для истории
|
|
||||||
- Оптимизации через таблицы быстрого поиска
|
|
||||||
|
|
||||||
**Сложность:** O(n) где n - размер выходных данных
|
|
||||||
@@ -1,608 +0,0 @@
|
|||||||
# Документация по формату NRes
|
|
||||||
|
|
||||||
## Обзор
|
|
||||||
|
|
||||||
NRes — это формат контейнера ресурсов, используемый в игровом движке Nikita. Файл представляет собой архив, содержащий несколько упакованных файлов с метаданными и поддержкой различных методов сжатия.
|
|
||||||
|
|
||||||
## Структура файла NRes
|
|
||||||
|
|
||||||
### 1. Заголовок файла (16 байт)
|
|
||||||
|
|
||||||
```c
|
|
||||||
struct NResHeader {
|
|
||||||
uint32_t signature; // +0x00: Сигнатура "NRes" (0x7365524E в little-endian)
|
|
||||||
uint32_t version; // +0x04: Версия формата (0x00000100 = версия 1.0)
|
|
||||||
uint32_t fileCount; // +0x08: Количество файлов в архиве
|
|
||||||
uint32_t fileSize; // +0x0C: Общий размер файла в байтах
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
**Детали:**
|
|
||||||
|
|
||||||
- `signature`: Константа `0x7365524E` (1936020046 в десятичном виде). Это ASCII строка "NRes" в обратном порядке байт
|
|
||||||
- `version`: Всегда должна быть `0x00000100` (256 в десятичном виде) для версии 1.0
|
|
||||||
- `fileCount`: Общее количество файлов в архиве (используется для валидации)
|
|
||||||
- `fileSize`: Полный размер NRes файла, включая заголовок
|
|
||||||
|
|
||||||
### 2. Данные файлов
|
|
||||||
|
|
||||||
Сразу после заголовка (с offset 0x10) начинаются данные упакованных файлов. Они хранятся последовательно, один за другим. Точное расположение каждого файла определяется записью в каталоге (см. раздел 3).
|
|
||||||
|
|
||||||
**⚠️ ВАЖНО: Выравнивание данных**
|
|
||||||
|
|
||||||
Данные каждого файла **выравниваются по границе 8 байт**. После записи данных файла добавляется padding (нулевые байты) до ближайшего кратного 8 адреса.
|
|
||||||
|
|
||||||
**Формула выравнивания:**
|
|
||||||
|
|
||||||
```
|
|
||||||
aligned_size = (packed_size + 7) & ~7
|
|
||||||
padding_bytes = aligned_size - packed_size
|
|
||||||
```
|
|
||||||
|
|
||||||
**Пример:**
|
|
||||||
|
|
||||||
- Файл размером 100 байт → padding 4 байта (до 104)
|
|
||||||
- Файл размером 104 байт → padding 0 байт (уже выровнен)
|
|
||||||
- Файл размером 105 байт → padding 3 байта (до 108)
|
|
||||||
|
|
||||||
Это означает, что:
|
|
||||||
|
|
||||||
1. `dataOffset` следующего файла всегда кратен 8
|
|
||||||
2. Между данными файлов могут быть 0-7 байт нулевого padding
|
|
||||||
3. При чтении нужно использовать `packedSize`, а не выравнивать вручную
|
|
||||||
|
|
||||||
### 3. Каталог файлов (Directory)
|
|
||||||
|
|
||||||
Каталог находится в **конце файла**. Его расположение вычисляется по формуле:
|
|
||||||
|
|
||||||
```
|
|
||||||
DirectoryOffset = FileSize - (FileCount * 64)
|
|
||||||
```
|
|
||||||
|
|
||||||
Каждая запись в каталоге имеет **фиксированный размер 64 байта (0x40)**:
|
|
||||||
|
|
||||||
```c
|
|
||||||
struct NResFileEntry {
|
|
||||||
char name[16]; // +0x00: Имя файла (NULL-terminated, uppercase)
|
|
||||||
uint32_t crc32; // +0x10: CRC32 хеш упакованных данных
|
|
||||||
uint32_t packMethod; // +0x14: Флаги метода упаковки (также используется как XOR seed)
|
|
||||||
uint32_t unpackedSize; // +0x18: Размер файла после распаковки
|
|
||||||
uint32_t packedSize; // +0x1C: Размер упакованных данных
|
|
||||||
uint32_t dataOffset; // +0x20: Смещение данных от начала файла
|
|
||||||
uint32_t fastDataPtr; // +0x24: Указатель для быстрого доступа (в памяти)
|
|
||||||
uint32_t xorSize; // +0x28: Размер данных для XOR-шифрования
|
|
||||||
uint32_t sortIndex; // +0x2C: Индекс для сортировки по имени
|
|
||||||
uint32_t reserved[4]; // +0x30: Зарезервировано (обычно нули)
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
## Подробное описание полей каталога
|
|
||||||
|
|
||||||
### Поле: name (смещение +0x00, 16 байт)
|
|
||||||
|
|
||||||
- **Назначение**: Имя файла в архиве
|
|
||||||
- **Формат**: NULL-terminated строка, максимум 15 символов + NULL
|
|
||||||
- **Особенности**:
|
|
||||||
- Все символы хранятся в **UPPERCASE** (заглавными буквами)
|
|
||||||
- При поиске файлов используется регистронезависимое сравнение (`_strcmpi`)
|
|
||||||
- Если имя короче 16 байт, остаток заполняется нулями
|
|
||||||
|
|
||||||
### Поле: crc32 (смещение +0x10, 4 байта)
|
|
||||||
|
|
||||||
- **Назначение**: Контрольная сумма CRC32 упакованных данных
|
|
||||||
- **Использование**: Проверка целостности данных при чтении
|
|
||||||
|
|
||||||
### Поле: packMethod (смещение +0x14, 4 байта)
|
|
||||||
|
|
||||||
**Критически важное поле!** Содержит битовые флаги, определяющие метод обработки данных:
|
|
||||||
|
|
||||||
```c
|
|
||||||
// Маски для извлечения метода упаковки
|
|
||||||
#define PACK_METHOD_MASK 0x1E0 // Биты 5-8 (метод + XOR)
|
|
||||||
#define PACK_METHOD_MASK2 0x1C0 // Биты 6-7 (без XOR-бита)
|
|
||||||
|
|
||||||
// Методы упаковки (packMethod & 0x1E0)
|
|
||||||
#define PACK_NONE 0x000 // Нет упаковки (raw)
|
|
||||||
#define PACK_XOR 0x020 // XOR (только шифрование)
|
|
||||||
#define PACK_FRES 0x040 // FRES (LZSS простой режим)
|
|
||||||
#define PACK_FRES_XOR 0x060 // XOR + FRES
|
|
||||||
#define PACK_LZHUF 0x080 // LZHUF (LZSS + adaptive Huffman)
|
|
||||||
#define PACK_LZHUF_XOR 0x0A0 // XOR + LZHUF
|
|
||||||
#define PACK_DEFLATE_RAW 0x100 // RAW-DEFLATE (без zlib-обёртки)
|
|
||||||
```
|
|
||||||
|
|
||||||
**Алгоритм определения метода:**
|
|
||||||
|
|
||||||
1. Извлечь биты `packMethod & 0x1E0`
|
|
||||||
2. Проверить конкретные значения:
|
|
||||||
- `0x000`: Данные не сжаты, простое копирование
|
|
||||||
- `0x020`: XOR-шифрование с двухбайтовым ключом
|
|
||||||
- `0x040` или `0x060`: FRES (может быть + XOR)
|
|
||||||
- `0x080` или `0x0A0`: LZHUF (может быть + XOR)
|
|
||||||
- `0x100`: RAW-DEFLATE (inflate без zlib-обёртки)
|
|
||||||
|
|
||||||
**Важно:** `rsGetPackMethod()` возвращает `packMethod & 0x1C0`, то есть маску **без XOR-бита `0x20`**. Это нужно учитывать при сравнении.
|
|
||||||
|
|
||||||
**Примечание про XOR seed:** значение для XOR берётся из поля `packMethod` (смещение `+0x14`). Это же поле может быть перезаписано при формировании каталога (см. раздел о `rsOpenLib`), если в библиотеке нет готовой таблицы сортировки.
|
|
||||||
|
|
||||||
### Поле: unpackedSize (смещение +0x18, 4 байта)
|
|
||||||
|
|
||||||
- **Назначение**: Размер файла после полной распаковки
|
|
||||||
- **Использование**:
|
|
||||||
- Для выделения памяти под распакованные данные
|
|
||||||
- Для проверки корректности распаковки
|
|
||||||
|
|
||||||
### Поле: packedSize (смещение +0x1C, 4 байта)
|
|
||||||
|
|
||||||
- **Назначение**: Размер сжатых данных в архиве
|
|
||||||
- **Особенности**:
|
|
||||||
- Если `packedSize == 0`, файл пустой или является указателем
|
|
||||||
- Для несжатых файлов: `packedSize == unpackedSize`
|
|
||||||
|
|
||||||
### Поле: dataOffset (смещение +0x20, 4 байта)
|
|
||||||
|
|
||||||
- **Назначение**: Абсолютное смещение данных файла от начала NRes файла
|
|
||||||
- **Формула вычисления**: `BaseAddress + dataOffset = начало данных`
|
|
||||||
- **Диапазон**: Обычно от 0x10 (после заголовка) до начала каталога
|
|
||||||
|
|
||||||
### Поле: fastDataPtr (смещение +0x24, 4 байта)
|
|
||||||
|
|
||||||
- **Назначение**: Указатель на данные в памяти для быстрого доступа
|
|
||||||
- **Использование**: Только во время выполнения (runtime)
|
|
||||||
- **В файле**: Обычно равно 0 или содержит относительный offset
|
|
||||||
- **Особенность**: Используется функцией `rsLoadFast()` для файлов без упаковки
|
|
||||||
|
|
||||||
### Поле: xorSize (смещение +0x28, 4 байта)
|
|
||||||
|
|
||||||
- **Назначение**: Размер данных для XOR-шифрования при комбинированных методах
|
|
||||||
- **Использование**:
|
|
||||||
- Когда `packMethod & 0x60 == 0x60` (FRES + XOR)
|
|
||||||
- Сначала применяется XOR к этому количеству байт, затем FRES к результату
|
|
||||||
- **Значение**: Может отличаться от `packedSize` при многоэтапной упаковке
|
|
||||||
|
|
||||||
### Поле: sortIndex (смещение +0x2C, 4 байта)
|
|
||||||
|
|
||||||
- **Назначение**: Индекс для быстрого поиска по отсортированному каталогу
|
|
||||||
- **Использование**:
|
|
||||||
- В `rsOpenLib` при отсутствии маркера `0xABBA` формируется таблица индексов сортировки имён
|
|
||||||
- Индексы записываются в это поле с шагом 0x40 (по записи)
|
|
||||||
- Используется `rsFind()` через таблицу индексов, а не прямую сортировку записей
|
|
||||||
|
|
||||||
### Поле: reserved (смещение +0x30, 16 байт)
|
|
||||||
|
|
||||||
- **Назначение**: Зарезервировано для будущих расширений
|
|
||||||
- **В файле**: Обычно заполнено нулями
|
|
||||||
- **Может содержать**: Дополнительные метаданные в новых версиях формата
|
|
||||||
|
|
||||||
## Алгоритмы упаковки
|
|
||||||
|
|
||||||
### 1. Без упаковки (PACK_NONE = 0x000)
|
|
||||||
|
|
||||||
```
|
|
||||||
Простое копирование данных:
|
|
||||||
memcpy(destination, source, packedSize);
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. XOR-шифрование (PACK_XOR = 0x020)
|
|
||||||
|
|
||||||
```c
|
|
||||||
// Ключ/seed берется из поля packMethod (смещение +0x14)
|
|
||||||
uint16_t key = (uint16_t)(packMethod & 0xFFFF);
|
|
||||||
|
|
||||||
for (int i = 0; i < packedSize; i++) {
|
|
||||||
uint8_t byte = source[i];
|
|
||||||
destination[i] = byte ^ (key >> 8) ^ (key << 1);
|
|
||||||
|
|
||||||
// Обновление ключа
|
|
||||||
uint8_t newByte = (key >> 8) ^ (key << 1);
|
|
||||||
key = (newByte ^ ((key >> 8) >> 1)) | (newByte << 8);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Ключевые особенности:**
|
|
||||||
|
|
||||||
- Используется 16-битный ключ из младших байт поля `packMethod`
|
|
||||||
- Ключ изменяется после каждого байта по специальному алгоритму
|
|
||||||
- Операции: XOR с старшим байтом ключа и со сдвинутым значением
|
|
||||||
|
|
||||||
### 3. [FRES компрессия](fres_decompression.md) (PACK_FRES = 0x040, 0x060)
|
|
||||||
|
|
||||||
Алгоритм FRES — это RLE-подобное сжатие с особой кодировкой повторов:
|
|
||||||
|
|
||||||
```
|
|
||||||
sub_1001B22E() - функция декомпрессии FRES
|
|
||||||
- Читает управляющие байты
|
|
||||||
- Декодирует литералы и повторы
|
|
||||||
- Использует скользящее окно для ссылок
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. [LZHUF (adaptive Huffman)](fres_decompression.md) (PACK_LZHUF = 0x080, 0x0A0)
|
|
||||||
|
|
||||||
Наиболее сложный и эффективный метод:
|
|
||||||
|
|
||||||
**Процесс декодирования:**
|
|
||||||
|
|
||||||
1. Распаковка LZSS + adaptive Huffman (Okumura LZHUF)
|
|
||||||
2. Дерево обновляется после каждого символа
|
|
||||||
3. Match-символы преобразуются в длину и позицию
|
|
||||||
|
|
||||||
### 5. [RAW-DEFLATE](huffman_decompression.md) (PACK_DEFLATE_RAW = 0x100)
|
|
||||||
|
|
||||||
Это inflate без zlib-обёртки (без 2-байтового заголовка и Adler32).
|
|
||||||
|
|
||||||
## Высокоуровневая инструкция по реализации
|
|
||||||
|
|
||||||
### Этап 1: Открытие файла
|
|
||||||
|
|
||||||
```python
|
|
||||||
def open_nres_file(filepath):
|
|
||||||
with open(filepath, 'rb') as f:
|
|
||||||
# 1. Читаем заголовок (16 байт)
|
|
||||||
header_data = f.read(16)
|
|
||||||
signature, version, file_count, file_size = struct.unpack('<4I', header_data)
|
|
||||||
|
|
||||||
# 2. Проверяем сигнатуру
|
|
||||||
if signature != 0x7365524E: # "NRes"
|
|
||||||
raise ValueError("Неверная сигнатура файла")
|
|
||||||
|
|
||||||
# 3. Проверяем версию
|
|
||||||
if version != 0x100:
|
|
||||||
raise ValueError(f"Неподдерживаемая версия: {version}")
|
|
||||||
|
|
||||||
# 4. Вычисляем расположение каталога
|
|
||||||
directory_offset = file_size - (file_count * 64)
|
|
||||||
|
|
||||||
# 5. Читаем весь файл в память (или используем memory mapping)
|
|
||||||
f.seek(0)
|
|
||||||
file_data = f.read()
|
|
||||||
|
|
||||||
return {
|
|
||||||
'file_count': file_count,
|
|
||||||
'file_size': file_size,
|
|
||||||
'directory_offset': directory_offset,
|
|
||||||
'data': file_data
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Этап 2: Чтение каталога
|
|
||||||
|
|
||||||
```python
|
|
||||||
def read_directory(nres_file):
|
|
||||||
data = nres_file['data']
|
|
||||||
offset = nres_file['directory_offset']
|
|
||||||
file_count = nres_file['file_count']
|
|
||||||
|
|
||||||
entries = []
|
|
||||||
|
|
||||||
for i in range(file_count):
|
|
||||||
entry_offset = offset + (i * 64)
|
|
||||||
entry_data = data[entry_offset:entry_offset + 64]
|
|
||||||
|
|
||||||
# Парсим 64-байтовую запись
|
|
||||||
name = entry_data[0:16].decode('ascii').rstrip('\x00')
|
|
||||||
crc32, pack_method, unpacked_size, packed_size, data_offset, \
|
|
||||||
fast_ptr, xor_size, sort_index = struct.unpack('<8I', entry_data[16:48])
|
|
||||||
|
|
||||||
entry = {
|
|
||||||
'name': name,
|
|
||||||
'crc32': crc32,
|
|
||||||
'pack_method': pack_method,
|
|
||||||
'unpacked_size': unpacked_size,
|
|
||||||
'packed_size': packed_size,
|
|
||||||
'data_offset': data_offset,
|
|
||||||
'fast_data_ptr': fast_ptr,
|
|
||||||
'xor_size': xor_size,
|
|
||||||
'sort_index': sort_index
|
|
||||||
}
|
|
||||||
|
|
||||||
entries.append(entry)
|
|
||||||
|
|
||||||
return entries
|
|
||||||
```
|
|
||||||
|
|
||||||
### Этап 3: Поиск файла по имени
|
|
||||||
|
|
||||||
```python
|
|
||||||
def find_file(entries, filename):
|
|
||||||
# Имена в архиве хранятся в UPPERCASE
|
|
||||||
search_name = filename.upper()[:15]
|
|
||||||
|
|
||||||
# Используем бинарный поиск, так как каталог отсортирован
|
|
||||||
# Сортировка по sort_index восстанавливает алфавитный порядок
|
|
||||||
sorted_entries = sorted(entries, key=lambda e: e['sort_index'])
|
|
||||||
|
|
||||||
left, right = 0, len(sorted_entries) - 1
|
|
||||||
|
|
||||||
while left <= right:
|
|
||||||
mid = (left + right) // 2
|
|
||||||
mid_name = sorted_entries[mid]['name']
|
|
||||||
|
|
||||||
if mid_name == search_name:
|
|
||||||
return sorted_entries[mid]
|
|
||||||
elif mid_name < search_name:
|
|
||||||
left = mid + 1
|
|
||||||
else:
|
|
||||||
right = mid - 1
|
|
||||||
|
|
||||||
return None
|
|
||||||
```
|
|
||||||
|
|
||||||
### Этап 4: Извлечение данных файла
|
|
||||||
|
|
||||||
```python
|
|
||||||
def extract_file(nres_file, entry):
|
|
||||||
data = nres_file['data']
|
|
||||||
|
|
||||||
# 1. Получаем упакованные данные
|
|
||||||
packed_data = data[entry['data_offset']:
|
|
||||||
entry['data_offset'] + entry['packed_size']]
|
|
||||||
|
|
||||||
# 2. Определяем метод упаковки
|
|
||||||
pack_method = entry['pack_method'] & 0x1E0
|
|
||||||
|
|
||||||
# 3. Распаковываем в зависимости от метода
|
|
||||||
if pack_method == 0x000:
|
|
||||||
# Без упаковки
|
|
||||||
return unpack_none(packed_data)
|
|
||||||
|
|
||||||
elif pack_method == 0x020:
|
|
||||||
# XOR-шифрование
|
|
||||||
return unpack_xor(packed_data, entry['pack_method'], entry['unpacked_size'])
|
|
||||||
|
|
||||||
elif pack_method == 0x040 or pack_method == 0x060:
|
|
||||||
# FRES компрессия (может быть с XOR)
|
|
||||||
if pack_method == 0x060:
|
|
||||||
# Сначала XOR
|
|
||||||
temp_data = unpack_xor(packed_data, entry['pack_method'], entry['xor_size'])
|
|
||||||
return unpack_fres(temp_data, entry['unpacked_size'])
|
|
||||||
else:
|
|
||||||
return unpack_fres(packed_data, entry['unpacked_size'])
|
|
||||||
|
|
||||||
elif pack_method == 0x080 or pack_method == 0x0A0:
|
|
||||||
# LZHUF (может быть с XOR)
|
|
||||||
if pack_method == 0x0A0:
|
|
||||||
temp_data = unpack_xor(packed_data, entry['pack_method'], entry['xor_size'])
|
|
||||||
return unpack_lzhuf(temp_data, entry['unpacked_size'])
|
|
||||||
return unpack_lzhuf(packed_data, entry['unpacked_size'])
|
|
||||||
|
|
||||||
elif pack_method == 0x100:
|
|
||||||
# RAW-DEFLATE
|
|
||||||
return unpack_deflate_raw(packed_data, entry['unpacked_size'])
|
|
||||||
|
|
||||||
else:
|
|
||||||
raise ValueError(f"Неподдерживаемый метод упаковки: 0x{pack_method:X}")
|
|
||||||
```
|
|
||||||
|
|
||||||
### Этап 5: Реализация алгоритмов распаковки
|
|
||||||
|
|
||||||
```python
|
|
||||||
def unpack_none(data):
|
|
||||||
"""Без упаковки - просто возвращаем данные"""
|
|
||||||
return data
|
|
||||||
|
|
||||||
def unpack_xor(data, pack_method, size):
|
|
||||||
"""XOR-дешифрование с изменяющимся ключом"""
|
|
||||||
result = bytearray(size)
|
|
||||||
key = pack_method & 0xFFFF # Берем младшие 16 бит из поля packMethod
|
|
||||||
|
|
||||||
for i in range(min(size, len(data))):
|
|
||||||
byte = data[i]
|
|
||||||
|
|
||||||
# XOR операция
|
|
||||||
high_byte = (key >> 8) & 0xFF
|
|
||||||
shifted = (key << 1) & 0xFFFF
|
|
||||||
result[i] = byte ^ high_byte ^ (shifted & 0xFF)
|
|
||||||
|
|
||||||
# Обновление ключа
|
|
||||||
new_byte = high_byte ^ (key << 1)
|
|
||||||
key = (new_byte ^ (high_byte >> 1)) | ((new_byte & 0xFF) << 8)
|
|
||||||
key &= 0xFFFF
|
|
||||||
|
|
||||||
return bytes(result)
|
|
||||||
|
|
||||||
def unpack_fres(data, unpacked_size):
|
|
||||||
"""
|
|
||||||
FRES декомпрессия - гибридный RLE+LZ77 алгоритм
|
|
||||||
Полная реализация в nres_decompression.py (класс FRESDecoder)
|
|
||||||
"""
|
|
||||||
from nres_decompression import FRESDecoder
|
|
||||||
decoder = FRESDecoder()
|
|
||||||
return decoder.decompress(data, unpacked_size)
|
|
||||||
|
|
||||||
def unpack_lzhuf(data, unpacked_size):
|
|
||||||
"""
|
|
||||||
LZHUF (LZSS + adaptive Huffman)
|
|
||||||
Полная реализация в nres_decompression.py (класс LZHUDecoder)
|
|
||||||
"""
|
|
||||||
from nres_decompression import LZHUDecoder
|
|
||||||
decoder = LZHUDecoder()
|
|
||||||
return decoder.decompress(data, unpacked_size)
|
|
||||||
|
|
||||||
def unpack_deflate_raw(data, unpacked_size):
|
|
||||||
"""
|
|
||||||
RAW-DEFLATE (inflate без zlib-обертки)
|
|
||||||
Полная реализация в nres_decompression.py (класс RawDeflateDecoder)
|
|
||||||
"""
|
|
||||||
from nres_decompression import RawDeflateDecoder
|
|
||||||
decoder = RawDeflateDecoder()
|
|
||||||
return decoder.decompress(data, unpacked_size)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Этап 6: Извлечение всех файлов
|
|
||||||
|
|
||||||
```python
|
|
||||||
def extract_all(nres_filepath, output_dir):
|
|
||||||
import os
|
|
||||||
|
|
||||||
# 1. Открываем NRes файл
|
|
||||||
nres_file = open_nres_file(nres_filepath)
|
|
||||||
|
|
||||||
# 2. Читаем каталог
|
|
||||||
entries = read_directory(nres_file)
|
|
||||||
|
|
||||||
# 3. Создаем выходную директорию
|
|
||||||
os.makedirs(output_dir, exist_ok=True)
|
|
||||||
|
|
||||||
# 4. Извлекаем каждый файл
|
|
||||||
for entry in entries:
|
|
||||||
print(f"Извлечение: {entry['name']}")
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Извлекаем данные
|
|
||||||
unpacked_data = extract_file(nres_file, entry)
|
|
||||||
|
|
||||||
# Сохраняем в файл
|
|
||||||
output_path = os.path.join(output_dir, entry['name'])
|
|
||||||
with open(output_path, 'wb') as f:
|
|
||||||
f.write(unpacked_data)
|
|
||||||
|
|
||||||
print(f" ✓ Успешно ({len(unpacked_data)} байт)")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f" ✗ Ошибка: {e}")
|
|
||||||
```
|
|
||||||
|
|
||||||
## Поддерживаемые контейнеры
|
|
||||||
|
|
||||||
### 1. NRes (MAGIC "NRes")
|
|
||||||
|
|
||||||
- Открывается через `niOpenResFile/niOpenResInMem`
|
|
||||||
- Каталог находится в конце файла (см. структуру выше)
|
|
||||||
|
|
||||||
### 2. rsLib / NL (MAGIC "NL")
|
|
||||||
|
|
||||||
Отдельный формат контейнера, обрабатывается `rsOpenLib`:
|
|
||||||
|
|
||||||
- В начале файла проверяется `*(_WORD*)buf == 0x4C4E` (ASCII "NL" в little-endian)
|
|
||||||
- `buf[3] == 1` — версия/маркер
|
|
||||||
- `buf[2]` — количество записей
|
|
||||||
- Каталог расположен с offset `0x20`, размер `0x20 * count`
|
|
||||||
- Каталог перед разбором дешифруется (байтовый XOR-поток)
|
|
||||||
|
|
||||||
## Поиск по имени (rsFind)
|
|
||||||
|
|
||||||
- Имя обрезается до 16 байт, `name[15] = 0`
|
|
||||||
- Приводится к верхнему регистру (`_strupr`)
|
|
||||||
- Поиск идёт по таблице индексов сортировки (значение хранится в поле `sortIndex`)
|
|
||||||
- Если в rsLib нет маркера `0xABBA`, таблица строится пузырьковой сортировкой и индексы записываются в поле записи
|
|
||||||
|
|
||||||
## Особенности и важные замечания
|
|
||||||
|
|
||||||
### 1. Порядок байт (Endianness)
|
|
||||||
|
|
||||||
- **Все многобайтовые значения хранятся в Little-Endian порядке**
|
|
||||||
- При чтении используйте `struct.unpack('<...')`
|
|
||||||
|
|
||||||
### 2. Сортировка каталога
|
|
||||||
|
|
||||||
- Каталог файлов **отсортирован по имени файла** (алфавитный порядок)
|
|
||||||
- Поле `sortIndex` хранит оригинальный индекс до сортировки
|
|
||||||
- Это позволяет использовать бинарный поиск
|
|
||||||
|
|
||||||
### 3. Регистр символов
|
|
||||||
|
|
||||||
- Все имена файлов конвертируются в **UPPERCASE** (заглавные буквы)
|
|
||||||
- При поиске используйте регистронезависимое сравнение
|
|
||||||
|
|
||||||
### 4. Memory Mapping
|
|
||||||
|
|
||||||
- Оригинальный код использует `MapViewOfFile` для эффективной работы с большими файлами
|
|
||||||
- Рекомендуется использовать memory-mapped файлы для больших архивов
|
|
||||||
|
|
||||||
### 5. Валидация данных
|
|
||||||
|
|
||||||
- **Всегда проверяйте сигнатуру** перед обработкой
|
|
||||||
- **Проверяйте версию** формата
|
|
||||||
- **Проверяйте CRC32** после распаковки
|
|
||||||
- **Проверяйте размеры** (unpacked_size должен совпадать с результатом)
|
|
||||||
|
|
||||||
### 6. Обработка ошибок
|
|
||||||
|
|
||||||
- Файл может быть поврежден
|
|
||||||
- Метод упаковки может быть неподдерживаемым
|
|
||||||
- Данные могут быть частично зашифрованы
|
|
||||||
|
|
||||||
### 7. Производительность
|
|
||||||
|
|
||||||
- Для несжатых файлов (`packMethod & 0x1E0 == 0`) можно использовать прямое чтение
|
|
||||||
- Поле `fastDataPtr` может содержать кешированный указатель
|
|
||||||
- Используйте буферизацию при последовательном чтении
|
|
||||||
|
|
||||||
### 8. Выравнивание данных
|
|
||||||
|
|
||||||
- **Все данные файлов выравниваются по 8 байт**
|
|
||||||
- После каждого файла может быть 0-7 байт нулевого padding
|
|
||||||
- `dataOffset` следующего файла всегда кратен 8
|
|
||||||
- При чтении используйте `packedSize` из записи, не вычисляйте выравнивание
|
|
||||||
- При создании архива добавляйте padding: `padding = ((size + 7) & ~7) - size`
|
|
||||||
|
|
||||||
## Пример использования
|
|
||||||
|
|
||||||
```python
|
|
||||||
# Открыть архив
|
|
||||||
nres = open_nres_file("resources.nres")
|
|
||||||
|
|
||||||
# Прочитать каталог
|
|
||||||
entries = read_directory(nres)
|
|
||||||
|
|
||||||
# Вывести список файлов
|
|
||||||
for entry in entries:
|
|
||||||
print(f"{entry['name']:20s} - {entry['unpacked_size']:8d} байт")
|
|
||||||
|
|
||||||
# Найти конкретный файл
|
|
||||||
entry = find_file(entries, "texture.bmp")
|
|
||||||
if entry:
|
|
||||||
data = extract_file(nres, entry)
|
|
||||||
with open("extracted_texture.bmp", "wb") as f:
|
|
||||||
f.write(data)
|
|
||||||
|
|
||||||
# Извлечь все файлы
|
|
||||||
extract_all("resources.nres", "./extracted/")
|
|
||||||
```
|
|
||||||
|
|
||||||
## Дополнительные функции
|
|
||||||
|
|
||||||
### Проверка формата файла
|
|
||||||
|
|
||||||
```python
|
|
||||||
def is_nres_file(filepath):
|
|
||||||
try:
|
|
||||||
with open(filepath, 'rb') as f:
|
|
||||||
signature = struct.unpack('<I', f.read(4))[0]
|
|
||||||
return signature == 0x7365524E
|
|
||||||
except:
|
|
||||||
return False
|
|
||||||
```
|
|
||||||
|
|
||||||
### Получение информации о файле
|
|
||||||
|
|
||||||
```python
|
|
||||||
def get_file_info(entry):
|
|
||||||
pack_names = {
|
|
||||||
0x000: "Без сжатия",
|
|
||||||
0x020: "XOR",
|
|
||||||
0x040: "FRES",
|
|
||||||
0x060: "FRES+XOR",
|
|
||||||
0x080: "LZHUF",
|
|
||||||
0x0A0: "LZHUF+XOR",
|
|
||||||
0x100: "RAW-DEFLATE"
|
|
||||||
}
|
|
||||||
|
|
||||||
pack_method = entry['pack_method'] & 0x1E0
|
|
||||||
pack_name = pack_names.get(pack_method, f"Неизвестный (0x{pack_method:X})")
|
|
||||||
|
|
||||||
ratio = 100.0 * entry['packed_size'] / entry['unpacked_size'] if entry['unpacked_size'] > 0 else 0
|
|
||||||
|
|
||||||
return {
|
|
||||||
'name': entry['name'],
|
|
||||||
'size': entry['unpacked_size'],
|
|
||||||
'packed': entry['packed_size'],
|
|
||||||
'compression': pack_name,
|
|
||||||
'ratio': f"{ratio:.1f}%",
|
|
||||||
'crc32': f"0x{entry['crc32']:08X}"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Заключение
|
|
||||||
|
|
||||||
Формат NRes представляет собой эффективный архив с поддержкой множества методов сжатия.
|
|
||||||
705
docs/specs/nres.md
Normal file
705
docs/specs/nres.md
Normal file
@@ -0,0 +1,705 @@
|
|||||||
|
# Форматы игровых ресурсов
|
||||||
|
|
||||||
|
## Обзор
|
||||||
|
|
||||||
|
Библиотека `Ngi32.dll` реализует два различных формата архивов ресурсов:
|
||||||
|
|
||||||
|
1. **NRes** — основной формат архива ресурсов, используемый через API `niOpenResFile` / `niCreateResFile`. Каталог файлов расположен в **конце** файла. Поддерживает создание, редактирование, добавление и удаление записей.
|
||||||
|
|
||||||
|
2. **RsLi** — формат библиотеки ресурсов, используемый через API `rsOpenLib` / `rsLoad`. Таблица записей расположена **в начале** файла (сразу после заголовка) и зашифрована XOR-шифром. Поддерживает несколько методов сжатия. Только чтение.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Часть 1. Формат NRes
|
||||||
|
|
||||||
|
## 1.1. Общая структура файла
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────────────────┐ Смещение 0
|
||||||
|
│ Заголовок (16 байт) │
|
||||||
|
├──────────────────────────┤ Смещение 16
|
||||||
|
│ │
|
||||||
|
│ Данные ресурсов │
|
||||||
|
│ (выровнены по 8 байт) │
|
||||||
|
│ │
|
||||||
|
├──────────────────────────┤ Смещение = total_size - entry_count × 64
|
||||||
|
│ Каталог записей │
|
||||||
|
│ (entry_count × 64 байт) │
|
||||||
|
└──────────────────────────┘ Смещение = total_size
|
||||||
|
```
|
||||||
|
|
||||||
|
## 1.2. Заголовок файла (16 байт)
|
||||||
|
|
||||||
|
| Смещение | Размер | Тип | Значение | Описание |
|
||||||
|
| -------- | ------ | ------- | ------------------- | ------------------------------------ |
|
||||||
|
| 0 | 4 | char[4] | `NRes` (0x4E526573) | Магическая сигнатура (little-endian) |
|
||||||
|
| 4 | 4 | uint32 | `0x00000100` (256) | Версия формата (1.0) |
|
||||||
|
| 8 | 4 | int32 | — | Количество записей в каталоге |
|
||||||
|
| 12 | 4 | int32 | — | Полный размер файла в байтах |
|
||||||
|
|
||||||
|
**Валидация при открытии:** магическая сигнатура и версия должны совпадать точно. Поле `total_size` (смещение 12) **проверяется на равенство** с фактическим размером файла (`GetFileSize`). Если значения не совпадают — файл отклоняется.
|
||||||
|
|
||||||
|
## 1.3. Положение каталога в файле
|
||||||
|
|
||||||
|
Каталог располагается в самом конце файла. Его смещение вычисляется по формуле:
|
||||||
|
|
||||||
|
```
|
||||||
|
directory_offset = total_size - entry_count × 64
|
||||||
|
```
|
||||||
|
|
||||||
|
Данные ресурсов занимают пространство между заголовком (16 байт) и каталогом.
|
||||||
|
|
||||||
|
## 1.4. Запись каталога (64 байта)
|
||||||
|
|
||||||
|
Каждая запись каталога занимает ровно **64 байта** (0x40):
|
||||||
|
|
||||||
|
| Смещение | Размер | Тип | Описание |
|
||||||
|
| -------- | ------ | -------- | ------------------------------------------------- |
|
||||||
|
| 0 | 4 | uint32 | Тип / идентификатор ресурса |
|
||||||
|
| 4 | 4 | uint32 | Атрибут 1 (например, формат, дата, категория) |
|
||||||
|
| 8 | 4 | uint32 | Атрибут 2 (например, подтип, метка времени) |
|
||||||
|
| 12 | 4 | uint32 | Размер данных ресурса в байтах |
|
||||||
|
| 16 | 4 | uint32 | Атрибут 3 (дополнительный параметр) |
|
||||||
|
| 20 | 36 | char[36] | Имя файла (null-terminated, макс. 35 символов) |
|
||||||
|
| 56 | 4 | uint32 | Смещение данных от начала файла |
|
||||||
|
| 60 | 4 | uint32 | Индекс сортировки (для двоичного поиска по имени) |
|
||||||
|
|
||||||
|
### Поле «Имя файла» (смещение 20, 36 байт)
|
||||||
|
|
||||||
|
- Максимальная длина имени: **35 символов** + 1 байт null-терминатор.
|
||||||
|
- При записи поле сначала обнуляется (`memset(0, 36 байт)`), затем копируется имя (`strncpy`, макс. 35 символов).
|
||||||
|
- Поиск по имени выполняется **без учёта регистра** (`_strcmpi`).
|
||||||
|
|
||||||
|
### Поле «Индекс сортировки» (смещение 60)
|
||||||
|
|
||||||
|
Используется для **двоичного поиска по имени**. Содержит индекс оригинальной записи, отсортированной в алфавитном порядке (регистронезависимо). Индекс строится при сохранении файла функцией `sub_10013260` с помощью **пузырьковой сортировки** по именам.
|
||||||
|
|
||||||
|
**Алгоритм поиска** (`sub_10011E60`): классический двоичный поиск по отсортированному массиву индексов. Возвращает оригинальный индекс записи или `-1` при отсутствии.
|
||||||
|
|
||||||
|
### Поле «Смещение данных» (смещение 56)
|
||||||
|
|
||||||
|
Абсолютное смещение от начала файла. Данные читаются из mapped view: `pointer = mapped_base + data_offset`.
|
||||||
|
|
||||||
|
## 1.5. Выравнивание данных
|
||||||
|
|
||||||
|
При добавлении ресурса его данные записываются последовательно, после чего выполняется **выравнивание по 8-байтной границе**:
|
||||||
|
|
||||||
|
```c
|
||||||
|
padding = ((data_size + 7) & ~7) - data_size;
|
||||||
|
// Если padding > 0, записываются нулевые байты
|
||||||
|
```
|
||||||
|
|
||||||
|
Таким образом, каждый блок данных начинается с адреса, кратного 8.
|
||||||
|
|
||||||
|
При изменении размера данных ресурса выполняется сдвиг всех последующих данных и обновление смещений всех затронутых записей каталога.
|
||||||
|
|
||||||
|
## 1.6. Создание файла (API `niCreateResFile`)
|
||||||
|
|
||||||
|
При создании нового файла:
|
||||||
|
|
||||||
|
1. Если файл уже существует и содержит корректный NRes-архив, существующий каталог считывается с конца файла, а файл усекается до начала каталога.
|
||||||
|
2. Если файл пуст или не является NRes-архивом, создаётся новый с пустым каталогом. Поля `entry_count = 0`, `total_size = 16`.
|
||||||
|
|
||||||
|
При закрытии файла (`sub_100122D0`):
|
||||||
|
|
||||||
|
1. Заголовок переписывается в начало файла (16 байт).
|
||||||
|
2. Вычисляется `total_size = data_end_offset + entry_count × 64`.
|
||||||
|
3. Индексы сортировки пересчитываются.
|
||||||
|
4. Каталог записей записывается в конец файла.
|
||||||
|
|
||||||
|
## 1.7. Режимы сортировки каталога
|
||||||
|
|
||||||
|
Функция `sub_10012560` поддерживает 12 режимов сортировки (0–11):
|
||||||
|
|
||||||
|
| Режим | Порядок сортировки |
|
||||||
|
| ----- | --------------------------------- |
|
||||||
|
| 0 | Без сортировки (сброс) |
|
||||||
|
| 1 | По атрибуту 1 (смещение 4) |
|
||||||
|
| 2 | По атрибуту 2 (смещение 8) |
|
||||||
|
| 3 | По (атрибут 1, атрибут 2) |
|
||||||
|
| 4 | По типу ресурса (смещение 0) |
|
||||||
|
| 5 | По (тип, атрибут 1) |
|
||||||
|
| 6 | По (тип, атрибут 1) — идентичен 5 |
|
||||||
|
| 7 | По (тип, атрибут 1, атрибут 2) |
|
||||||
|
| 8 | По имени (регистронезависимо) |
|
||||||
|
| 9 | По (тип, имя) |
|
||||||
|
| 10 | По (атрибут 1, имя) |
|
||||||
|
| 11 | По (атрибут 2, имя) |
|
||||||
|
|
||||||
|
## 1.8. Операция `niOpenResFileEx` — флаги открытия
|
||||||
|
|
||||||
|
Второй параметр — битовые флаги:
|
||||||
|
|
||||||
|
| Бит | Маска | Описание |
|
||||||
|
| --- | ----- | ----------------------------------------------------------------------------------- |
|
||||||
|
| 0 | 0x01 | Sequential scan hint (`FILE_FLAG_SEQUENTIAL_SCAN` вместо `FILE_FLAG_RANDOM_ACCESS`) |
|
||||||
|
| 1 | 0x02 | Открыть для записи (read-write). Без флага — только чтение |
|
||||||
|
| 2 | 0x04 | Пометить файл как «кэшируемый» (не выгружать при refcount=0) |
|
||||||
|
| 3 | 0x08 | Raw-режим: не проверять заголовок NRes, трактовать весь файл как единый ресурс |
|
||||||
|
|
||||||
|
## 1.9. Виртуальное касание страниц
|
||||||
|
|
||||||
|
Функция `sub_100197D0` выполняет «касание» страниц памяти для принудительной загрузки из memory-mapped файла. Она обходит адресное пространство с шагом 4096 байт (размер страницы), начиная с 0x10000 (64 КБ):
|
||||||
|
|
||||||
|
```
|
||||||
|
for (result = 0x10000; result < size; result += 4096);
|
||||||
|
```
|
||||||
|
|
||||||
|
Вызывается при чтении данных ресурса с флагом `a3 != 0` для предзагрузки данных в оперативную память.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Часть 2. Формат RsLi
|
||||||
|
|
||||||
|
## 2.1. Общая структура файла
|
||||||
|
|
||||||
|
```
|
||||||
|
┌───────────────────────────────┐ Смещение 0
|
||||||
|
│ Заголовок файла (32 байта) │
|
||||||
|
├───────────────────────────────┤ Смещение 32
|
||||||
|
│ Таблица записей (зашифрована)│
|
||||||
|
│ (entry_count × 32 байт) │
|
||||||
|
├───────────────────────────────┤ Смещение 32 + entry_count × 32
|
||||||
|
│ │
|
||||||
|
│ Данные ресурсов │
|
||||||
|
│ │
|
||||||
|
├───────────────────────────────┤
|
||||||
|
│ [Опциональный трейлер — 6 б] │
|
||||||
|
└───────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2.2. Заголовок файла (32 байта)
|
||||||
|
|
||||||
|
| Смещение | Размер | Тип | Значение | Описание |
|
||||||
|
| -------- | ------ | ------- | ----------------- | --------------------------------------------- |
|
||||||
|
| 0 | 2 | char[2] | `NL` (0x4C4E) | Магическая сигнатура |
|
||||||
|
| 2 | 1 | uint8 | `0x00` | Зарезервировано (должно быть 0) |
|
||||||
|
| 3 | 1 | uint8 | `0x01` | Версия формата |
|
||||||
|
| 4 | 2 | int16 | — | Количество записей (sign-extended при чтении) |
|
||||||
|
| 6 | 8 | — | — | Зарезервировано / не используется |
|
||||||
|
| 14 | 2 | uint16 | `0xABBA` или иное | Флаг предсортировки (см. ниже) |
|
||||||
|
| 16 | 4 | — | — | Зарезервировано |
|
||||||
|
| 20 | 4 | uint32 | — | **Начальное состояние XOR-шифра** (seed) |
|
||||||
|
| 24 | 8 | — | — | Зарезервировано |
|
||||||
|
|
||||||
|
### Флаг предсортировки (смещение 14)
|
||||||
|
|
||||||
|
- Если `*(uint16*)(header + 14) == 0xABBA` — движок **не строит** таблицу индексов в памяти. Значения `entry[i].sort_to_original` используются **как есть** (и для двоичного поиска, и как XOR‑ключ для данных).
|
||||||
|
- Если значение **отлично от 0xABBA** — после загрузки выполняется **пузырьковая сортировка** имён и строится перестановка `sort_to_original[]`, которая затем **записывается в `entry[i].sort_to_original`**, перетирая значения из файла. Именно эта перестановка далее используется и для поиска, и как XOR‑ключ (младшие 16 бит).
|
||||||
|
|
||||||
|
## 2.3. XOR-шифр таблицы записей
|
||||||
|
|
||||||
|
Таблица записей начинается со смещения 32 и зашифрована поточным XOR-шифром. Ключ инициализируется из DWORD по смещению 20 заголовка.
|
||||||
|
|
||||||
|
### Начальное состояние
|
||||||
|
|
||||||
|
```
|
||||||
|
seed = *(uint32*)(header + 20)
|
||||||
|
lo = seed & 0xFF // Младший байт
|
||||||
|
hi = (seed >> 8) & 0xFF // Второй байт
|
||||||
|
```
|
||||||
|
|
||||||
|
### Алгоритм дешифровки (побайтовый)
|
||||||
|
|
||||||
|
Для каждого зашифрованного байта `encrypted[i]`, начиная с `i = 0`:
|
||||||
|
|
||||||
|
```
|
||||||
|
step 1: lo = hi ^ ((lo << 1) & 0xFF) // Сдвиг lo влево на 1, XOR с hi
|
||||||
|
step 2: decrypted[i] = lo ^ encrypted[i] // Расшифровка байта
|
||||||
|
step 3: hi = lo ^ ((hi >> 1) & 0xFF) // Сдвиг hi вправо на 1, XOR с lo
|
||||||
|
```
|
||||||
|
|
||||||
|
**Пример реализации:**
|
||||||
|
|
||||||
|
```python
|
||||||
|
def decrypt_rs_entries(encrypted_data: bytes, seed: int) -> bytes:
|
||||||
|
lo = seed & 0xFF
|
||||||
|
hi = (seed >> 8) & 0xFF
|
||||||
|
result = bytearray(len(encrypted_data))
|
||||||
|
for i in range(len(encrypted_data)):
|
||||||
|
lo = (hi ^ ((lo << 1) & 0xFF)) & 0xFF
|
||||||
|
result[i] = lo ^ encrypted_data[i]
|
||||||
|
hi = (lo ^ ((hi >> 1) & 0xFF)) & 0xFF
|
||||||
|
return bytes(result)
|
||||||
|
```
|
||||||
|
|
||||||
|
Этот же алгоритм используется для шифрования данных ресурсов с методом XOR (флаги 0x20, 0x60, 0xA0), но с другим начальным ключом из записи.
|
||||||
|
|
||||||
|
## 2.4. Запись таблицы (32 байта, на диске, до дешифровки)
|
||||||
|
|
||||||
|
После дешифровки каждая запись имеет следующую структуру:
|
||||||
|
|
||||||
|
| Смещение | Размер | Тип | Описание |
|
||||||
|
| -------- | ------ | -------- | -------------------------------------------------------------- |
|
||||||
|
| 0 | 12 | char[12] | Имя ресурса (ASCII, обычно uppercase; строка читается до `\0`) |
|
||||||
|
| 12 | 4 | — | Зарезервировано (движком игнорируется) |
|
||||||
|
| 16 | 2 | int16 | **Флаги** (метод сжатия и атрибуты) |
|
||||||
|
| 18 | 2 | int16 | **`sort_to_original[i]` / XOR‑ключ** (см. ниже) |
|
||||||
|
| 20 | 4 | uint32 | **Размер распакованных данных** (`unpacked_size`) |
|
||||||
|
| 24 | 4 | uint32 | Смещение данных от начала файла (`data_offset`) |
|
||||||
|
| 28 | 4 | uint32 | Размер упакованных данных в байтах (`packed_size`) |
|
||||||
|
|
||||||
|
### Имена ресурсов
|
||||||
|
|
||||||
|
- Поле `name[12]` копируется побайтно. Внутренне движок всегда имеет `\0` сразу после этих 12 байт (зарезервированные 4 байта в памяти принудительно обнуляются), поэтому имя **может быть длиной до 12 символов** даже без `\0` внутри `name[12]`.
|
||||||
|
- На практике имена обычно **uppercase ASCII**. `rsFind` приводит запрос к верхнему регистру (`_strupr`) и сравнивает побайтно.
|
||||||
|
- `rsFind` копирует имя запроса `strncpy(..., 16)` и принудительно ставит `\0` в `Destination[15]`, поэтому запрос длиннее 15 символов будет усечён.
|
||||||
|
|
||||||
|
### Поле `sort_to_original[i]` (смещение 18)
|
||||||
|
|
||||||
|
Это **не “свойство записи”**, а элемент таблицы индексов, по которой `rsFind` делает двоичный поиск:
|
||||||
|
|
||||||
|
- Таблица реализована “внутри записей”: значение берётся как `entry[i].sort_to_original` (где `i` — позиция двоичного поиска), а реальная запись для сравнения берётся как `entry[ sort_to_original[i] ]`.
|
||||||
|
- Тем же значением (младшие 16 бит) инициализируется XOR‑шифр данных для методов, где он используется (0x20/0x60/0xA0). Поэтому при упаковке/шифровании данных ключ должен совпадать с итоговым `sort_to_original[i]` (см. флаг 0xABBA в разделе 2.2).
|
||||||
|
|
||||||
|
Поиск выполняется **двоичным поиском** по этой таблице, с фолбэком на **линейный поиск** если двоичный не нашёл (поведение `rsFind`).
|
||||||
|
|
||||||
|
## 2.5. Поле флагов (смещение 16 записи)
|
||||||
|
|
||||||
|
Биты поля флагов кодируют метод сжатия и дополнительные атрибуты:
|
||||||
|
|
||||||
|
```
|
||||||
|
Биты [8:5] (маска 0x1E0): Метод сжатия/шифрования
|
||||||
|
Бит [6] (маска 0x040): Флаг realloc (буфер декомпрессии может быть больше)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Методы сжатия (биты 8–5, маска 0x1E0)
|
||||||
|
|
||||||
|
| Значение | Hex | Описание |
|
||||||
|
| -------- | ----- | --------------------------------------- |
|
||||||
|
| 0x000 | 0x00 | Без сжатия (копирование) |
|
||||||
|
| 0x020 | 0x20 | Только XOR-шифр |
|
||||||
|
| 0x040 | 0x40 | LZSS (простой вариант) |
|
||||||
|
| 0x060 | 0x60 | XOR-шифр + LZSS (простой вариант) |
|
||||||
|
| 0x080 | 0x80 | LZSS с адаптивным кодированием Хаффмана |
|
||||||
|
| 0x0A0 | 0xA0 | XOR-шифр + LZSS с Хаффманом |
|
||||||
|
| 0x100 | 0x100 | Deflate (аналог zlib/RFC 1951) |
|
||||||
|
|
||||||
|
Примечание: `rsGetPackMethod()` возвращает `flags & 0x1C0` (без бита 0x20). Поэтому:
|
||||||
|
|
||||||
|
- для 0x20 вернётся 0x00,
|
||||||
|
- для 0x60 вернётся 0x40,
|
||||||
|
- для 0xA0 вернётся 0x80.
|
||||||
|
|
||||||
|
### Бит 0x40 (выделение +0x12 и последующее `realloc`)
|
||||||
|
|
||||||
|
Бит 0x40 проверяется отдельно (`flags & 0x40`). Если он установлен, выходной буфер выделяется с запасом `+0x12` (18 байт), а после распаковки вызывается `realloc` для усечения до точного `unpacked_size`.
|
||||||
|
|
||||||
|
Важно: этот же бит входит в код методов 0x40/0x60, поэтому для них поведение “+0x12 и shrink” включено автоматически.
|
||||||
|
|
||||||
|
## 2.6. Размеры данных
|
||||||
|
|
||||||
|
В каждой записи на диске хранятся оба значения:
|
||||||
|
|
||||||
|
- `unpacked_size` (смещение 20) — размер распакованных данных.
|
||||||
|
- `packed_size` (смещение 28) — размер упакованных данных (байт во входном потоке для выбранного метода).
|
||||||
|
|
||||||
|
Для метода 0x00 (без сжатия) обычно `packed_size == unpacked_size`.
|
||||||
|
|
||||||
|
`rsGetInfo` возвращает именно `unpacked_size` (то, сколько байт выдаст `rsLoad`).
|
||||||
|
|
||||||
|
## 2.7. Опциональный трейлер медиа (6 байт)
|
||||||
|
|
||||||
|
При открытии с флагом `a2 & 2`:
|
||||||
|
|
||||||
|
| Смещение от конца | Размер | Тип | Описание |
|
||||||
|
| ----------------- | ------ | ------- | ----------------------- |
|
||||||
|
| −6 | 2 | char[2] | Сигнатура `AO` (0x4F41) |
|
||||||
|
| −4 | 4 | uint32 | Смещение медиа-оверлея |
|
||||||
|
|
||||||
|
Если трейлер присутствует, все смещения данных в записях корректируются: `effective_offset = entry_offset + media_overlay_offset`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Часть 3. Алгоритмы сжатия (формат RsLi)
|
||||||
|
|
||||||
|
## 3.1. XOR-шифр данных (метод 0x20)
|
||||||
|
|
||||||
|
Алгоритм идентичен XOR‑шифру таблицы записей (раздел 2.3), но начальный ключ берётся из `entry[i].sort_to_original` (смещение 18 записи, младшие 16 бит).
|
||||||
|
|
||||||
|
Важно про размер входа:
|
||||||
|
|
||||||
|
- В ветке **0x20** движок XOR‑ит ровно `unpacked_size` байт (и ожидает, что поток данных имеет ту же длину; на практике `packed_size == unpacked_size`).
|
||||||
|
- В ветках **0x60/0xA0** XOR применяется к **упакованному** потоку длиной `packed_size` перед декомпрессией.
|
||||||
|
|
||||||
|
### Инициализация
|
||||||
|
|
||||||
|
```
|
||||||
|
key16 = (uint16)entry.sort_to_original // int16 на диске по смещению 18
|
||||||
|
lo = key16 & 0xFF
|
||||||
|
hi = (key16 >> 8) & 0xFF
|
||||||
|
```
|
||||||
|
|
||||||
|
### Дешифровка (псевдокод)
|
||||||
|
|
||||||
|
```
|
||||||
|
for i in range(N): # N = unpacked_size (для 0x20) или packed_size (для 0x60/0xA0)
|
||||||
|
lo = (hi ^ ((lo << 1) & 0xFF)) & 0xFF
|
||||||
|
out[i] = in[i] ^ lo
|
||||||
|
hi = (lo ^ ((hi >> 1) & 0xFF)) & 0xFF
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3.2. LZSS — простой вариант (метод 0x40)
|
||||||
|
|
||||||
|
Классический алгоритм LZSS (Lempel-Ziv-Storer-Szymanski) с кольцевым буфером.
|
||||||
|
|
||||||
|
### Параметры
|
||||||
|
|
||||||
|
| Параметр | Значение |
|
||||||
|
| ----------------------------- | ------------------ |
|
||||||
|
| Размер кольцевого буфера | 4096 байт (0x1000) |
|
||||||
|
| Начальная позиция записи | 4078 (0xFEE) |
|
||||||
|
| Начальное заполнение | 0x20 (пробел) |
|
||||||
|
| Минимальная длина совпадения | 3 |
|
||||||
|
| Максимальная длина совпадения | 18 (4 бита + 3) |
|
||||||
|
|
||||||
|
### Алгоритм декомпрессии
|
||||||
|
|
||||||
|
```
|
||||||
|
Инициализация:
|
||||||
|
ring_buffer[0..4095] = 0x20 (заполнить пробелами)
|
||||||
|
ring_pos = 4078
|
||||||
|
flags_byte = 0
|
||||||
|
flags_bits_remaining = 0
|
||||||
|
|
||||||
|
Цикл (пока не заполнен выходной буфер И не исчерпан входной):
|
||||||
|
|
||||||
|
1. Если flags_bits_remaining == 0:
|
||||||
|
- Прочитать 1 байт из входного потока → flags_byte
|
||||||
|
- flags_bits_remaining = 8
|
||||||
|
|
||||||
|
Декодировать как:
|
||||||
|
- Старший бит устанавливается в 0x7F (маркер)
|
||||||
|
- Оставшиеся 7 бит — флаги текущей группы
|
||||||
|
|
||||||
|
Реально в коде: control_word = (flags_byte) | (0x7F << 8)
|
||||||
|
Каждый бит проверяется сдвигом вправо.
|
||||||
|
|
||||||
|
2. Проверить младший бит control_word:
|
||||||
|
|
||||||
|
Если бит = 1 (литерал):
|
||||||
|
- Прочитать 1 байт из входного потока → byte
|
||||||
|
- ring_buffer[ring_pos] = byte
|
||||||
|
- ring_pos = (ring_pos + 1) & 0xFFF
|
||||||
|
- Записать byte в выходной буфер
|
||||||
|
|
||||||
|
Если бит = 0 (ссылка):
|
||||||
|
- Прочитать 2 байта: low_byte, high_byte
|
||||||
|
- offset = low_byte | ((high_byte & 0x0F) << 8) // 12 бит
|
||||||
|
- length = ((high_byte >> 4) & 0x0F) + 3 // 4 бита + 3
|
||||||
|
- Скопировать length байт из ring_buffer[offset...]:
|
||||||
|
для j от 0 до length-1:
|
||||||
|
byte = ring_buffer[(offset + j) & 0xFFF]
|
||||||
|
ring_buffer[ring_pos] = byte
|
||||||
|
ring_pos = (ring_pos + 1) & 0xFFF
|
||||||
|
записать byte в выходной буфер
|
||||||
|
|
||||||
|
3. Сдвинуть control_word вправо на 1 бит
|
||||||
|
4. flags_bits_remaining -= 1
|
||||||
|
```
|
||||||
|
|
||||||
|
### Подробная раскладка пары ссылки (2 байта)
|
||||||
|
|
||||||
|
```
|
||||||
|
Байт 0 (low): OOOOOOOO (биты [7:0] смещения)
|
||||||
|
Байт 1 (high): LLLLOOOO L = длина − 3, O = биты [11:8] смещения
|
||||||
|
|
||||||
|
offset = low | ((high & 0x0F) << 8) // Диапазон: 0–4095
|
||||||
|
length = (high >> 4) + 3 // Диапазон: 3–18
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3.3. LZSS с адаптивным кодированием Хаффмана (метод 0x80)
|
||||||
|
|
||||||
|
Расширенный вариант LZSS, где литералы и длины совпадений кодируются с помощью адаптивного дерева Хаффмана.
|
||||||
|
|
||||||
|
### Параметры
|
||||||
|
|
||||||
|
| Параметр | Значение |
|
||||||
|
| -------------------------------- | ------------------------------ |
|
||||||
|
| Размер кольцевого буфера | 4096 байт |
|
||||||
|
| Начальная позиция записи | **4036** (0xFC4) |
|
||||||
|
| Начальное заполнение | 0x20 (пробел) |
|
||||||
|
| Количество листовых узлов дерева | 314 |
|
||||||
|
| Символы литералов | 0–255 (байты) |
|
||||||
|
| Символы длин | 256–313 (длина = символ − 253) |
|
||||||
|
| Начальная длина | 3 (при символе 256) |
|
||||||
|
| Максимальная длина | 60 (при символе 313) |
|
||||||
|
|
||||||
|
### Дерево Хаффмана
|
||||||
|
|
||||||
|
Дерево строится как **адаптивное** (dynamic, self-adjusting):
|
||||||
|
|
||||||
|
- **627 узлов**: 314 листовых + 313 внутренних.
|
||||||
|
- Все листья изначально имеют **вес 1**.
|
||||||
|
- Корень дерева — узел с индексом 0 (в массиве `parent`).
|
||||||
|
- После декодирования каждого символа дерево **обновляется** (функция `sub_1001B0AE`): вес узла инкрементируется, и при нарушении порядка узлы **переставляются** для поддержания свойства.
|
||||||
|
- При достижении суммарного веса **0x8000 (32768)** — все веса **делятся на 2** (с округлением вверх) и дерево полностью перестраивается.
|
||||||
|
|
||||||
|
### Кодирование позиции
|
||||||
|
|
||||||
|
Позиция в кольцевом буфере кодируется с помощью **d-кода** (таблица дистанций):
|
||||||
|
|
||||||
|
- 8 бит позиции ищутся в таблице `d_code[256]`, определяя базовое значение и количество дополнительных битов.
|
||||||
|
- Из потока считываются дополнительные биты, которые объединяются с базовым значением.
|
||||||
|
- Финальная позиция: `pos = (ring_pos − 1 − decoded_position) & 0xFFF`
|
||||||
|
|
||||||
|
**Таблицы инициализации** (d-коды):
|
||||||
|
|
||||||
|
```
|
||||||
|
Таблица базовых значений — byte_100371D0[6]:
|
||||||
|
{ 0x01, 0x03, 0x08, 0x0C, 0x18, 0x10 }
|
||||||
|
|
||||||
|
Таблица дополнительных битов — byte_100371D6[6]:
|
||||||
|
{ 0x20, 0x30, 0x40, 0x30, 0x30, 0x10 }
|
||||||
|
```
|
||||||
|
|
||||||
|
### Алгоритм декомпрессии (высокоуровневый)
|
||||||
|
|
||||||
|
```
|
||||||
|
Инициализация:
|
||||||
|
ring_buffer[0..4095] = 0x20
|
||||||
|
ring_pos = 4036
|
||||||
|
Инициализировать дерево Хаффмана (314 листьев, все веса = 1)
|
||||||
|
Инициализировать таблицы d-кодов
|
||||||
|
|
||||||
|
Цикл:
|
||||||
|
1. Декодировать символ из потока по дереву Хаффмана:
|
||||||
|
- Начать с корня
|
||||||
|
- Читать биты, спускаться по дереву (0 = левый, 1 = правый)
|
||||||
|
- Пока не достигнут лист → символ = лист − 627
|
||||||
|
|
||||||
|
2. Обновить дерево Хаффмана для декодированного символа
|
||||||
|
|
||||||
|
3. Если символ < 256 (литерал):
|
||||||
|
- ring_buffer[ring_pos] = символ
|
||||||
|
- ring_pos = (ring_pos + 1) & 0xFFF
|
||||||
|
- Записать символ в выходной буфер
|
||||||
|
|
||||||
|
4. Если символ >= 256 (ссылка):
|
||||||
|
- length = символ − 253
|
||||||
|
- Декодировать позицию через d-код:
|
||||||
|
a) Прочитать 8 бит из потока
|
||||||
|
b) Найти d-код и дополнительные биты по таблице
|
||||||
|
c) Прочитать дополнительные биты
|
||||||
|
d) position = (ring_pos − 1 − full_position) & 0xFFF
|
||||||
|
- Скопировать length байт из ring_buffer[position...]
|
||||||
|
|
||||||
|
5. Если выходной буфер заполнен → завершить
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3.4. XOR + LZSS (методы 0x60 и 0xA0)
|
||||||
|
|
||||||
|
Комбинированный метод: сначала XOR-дешифровка, затем LZSS-декомпрессия.
|
||||||
|
|
||||||
|
### Алгоритм
|
||||||
|
|
||||||
|
1. Выделить временный буфер размером `compressed_size` (поле из записи, смещение 28).
|
||||||
|
2. Дешифровать сжатые данные XOR-шифром (раздел 3.1) с ключом из записи во временный буфер.
|
||||||
|
3. Применить LZSS-декомпрессию (простую или с Хаффманом, в зависимости от конкретного метода) из временного буфера в выходной.
|
||||||
|
4. Освободить временный буфер.
|
||||||
|
|
||||||
|
- **0x60** — XOR + простой LZSS (раздел 3.2)
|
||||||
|
- **0xA0** — XOR + LZSS с Хаффманом (раздел 3.3)
|
||||||
|
|
||||||
|
### Начальное состояние XOR для данных
|
||||||
|
|
||||||
|
При комбинированном методе seed берётся из поля по смещению 20 записи (4-байтный). Однако ключ обрабатывается как 16-битный: `lo = seed & 0xFF`, `hi = (seed >> 8) & 0xFF`.
|
||||||
|
|
||||||
|
## 3.5. Deflate (метод 0x100)
|
||||||
|
|
||||||
|
Полноценная реализация алгоритма **Deflate** (RFC 1951) с блочной структурой.
|
||||||
|
|
||||||
|
### Общая структура
|
||||||
|
|
||||||
|
Данные состоят из последовательности блоков. Каждый блок начинается с:
|
||||||
|
|
||||||
|
- **1 бит** — `is_final`: признак последнего блока
|
||||||
|
- **2 бита** — `block_type`: тип блока
|
||||||
|
|
||||||
|
### Типы блоков
|
||||||
|
|
||||||
|
| block_type | Описание | Функция |
|
||||||
|
| ---------- | --------------------------- | ---------------- |
|
||||||
|
| 0 | Без сжатия (stored) | `sub_1001A750` |
|
||||||
|
| 1 | Фиксированные коды Хаффмана | `sub_1001A8C0` |
|
||||||
|
| 2 | Динамические коды Хаффмана | `sub_1001AA30` |
|
||||||
|
| 3 | Зарезервировано (ошибка) | Возвращает код 2 |
|
||||||
|
|
||||||
|
### Блок типа 0 (stored)
|
||||||
|
|
||||||
|
1. Отбросить оставшиеся биты до границы байта (выравнивание).
|
||||||
|
2. Прочитать 16 бит — `LEN` (длина блока).
|
||||||
|
3. Прочитать 16 бит — `NLEN` (дополнение длины, `NLEN == ~LEN & 0xFFFF`).
|
||||||
|
4. Проверить: `LEN == (uint16)(~NLEN)`. При несовпадении — ошибка.
|
||||||
|
5. Скопировать `LEN` байт из входного потока в выходной.
|
||||||
|
|
||||||
|
Декомпрессор использует внутренний буфер размером **32768 байт** (0x8000). При заполнении — промежуточная запись результата.
|
||||||
|
|
||||||
|
### Блок типа 1 (фиксированные коды)
|
||||||
|
|
||||||
|
Стандартные коды Deflate:
|
||||||
|
|
||||||
|
- Литералы/длины: 288 кодов
|
||||||
|
- 0–143: 8-битные коды
|
||||||
|
- 144–255: 9-битные коды
|
||||||
|
- 256–279: 7-битные коды
|
||||||
|
- 280–287: 8-битные коды
|
||||||
|
- Дистанции: 30 кодов, все 5-битные
|
||||||
|
|
||||||
|
Используются предопределённые таблицы длин и дистанций (`unk_100370AC`, `unk_1003712C` и соответствующие экстра-биты).
|
||||||
|
|
||||||
|
### Блок типа 2 (динамические коды)
|
||||||
|
|
||||||
|
1. Прочитать 5 бит → `HLIT` (количество литералов/длин − 257). Диапазон: 257–286.
|
||||||
|
2. Прочитать 5 бит → `HDIST` (количество дистанций − 1). Диапазон: 1–30.
|
||||||
|
3. Прочитать 4 бита → `HCLEN` (количество кодов длин − 4). Диапазон: 4–19.
|
||||||
|
4. Прочитать `HCLEN` × 3 бит — длины кодов для алфавита длин.
|
||||||
|
5. Построить дерево Хаффмана для алфавита длин (19 символов).
|
||||||
|
6. С помощью этого дерева декодировать длины кодов для литералов/длин и дистанций.
|
||||||
|
7. Построить два дерева Хаффмана: для литералов/длин и для дистанций.
|
||||||
|
8. Декодировать данные.
|
||||||
|
|
||||||
|
**Порядок кодов длин** (стандартный Deflate):
|
||||||
|
|
||||||
|
```
|
||||||
|
{ 16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15 }
|
||||||
|
```
|
||||||
|
|
||||||
|
Хранится в `dword_10037060`.
|
||||||
|
|
||||||
|
### Валидации
|
||||||
|
|
||||||
|
- `HLIT + 257 <= 286` (max 0x11E)
|
||||||
|
- `HDIST + 1 <= 30` (max 0x1E)
|
||||||
|
- При нарушении — возвращается ошибка 1.
|
||||||
|
|
||||||
|
## 3.6. Метод 0x00 (без сжатия)
|
||||||
|
|
||||||
|
Данные копируются «как есть» напрямую из файла. Вызывается через указатель на функцию `dword_1003A1B8` (фактически `memcpy` или аналог).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Часть 4. Внутренние структуры в памяти
|
||||||
|
|
||||||
|
## 4.1. Внутренняя структура NRes-архива (opened, 0x68 байт = 104)
|
||||||
|
|
||||||
|
```c
|
||||||
|
struct NResArchive { // Размер: 0x68 (104 байта)
|
||||||
|
void* vtable; // +0: Указатель на таблицу виртуальных методов
|
||||||
|
int32_t entry_count; // +4: Количество записей
|
||||||
|
void* mapped_base; // +8: Базовый адрес mapped view
|
||||||
|
void* directory_ptr; // +12: Указатель на каталог записей в памяти
|
||||||
|
char* filename; // +16: Путь к файлу (_strdup)
|
||||||
|
int32_t ref_count; // +20: Счётчик ссылок
|
||||||
|
uint32_t last_release_time; // +24: timeGetTime() при последнем Release
|
||||||
|
// +28..+91: Для raw-режима — встроенная запись (единственный File entry)
|
||||||
|
NResArchive* next; // +92: Следующий архив в связном списке
|
||||||
|
uint8_t is_writable; // +100: Файл открыт для записи
|
||||||
|
uint8_t is_cacheable; // +101: Не выгружать при refcount = 0
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4.2. Внутренняя структура RsLi-архива (56 + 64 × N байт)
|
||||||
|
|
||||||
|
```c
|
||||||
|
struct RsLibHeader { // 56 байт (14 DWORD)
|
||||||
|
uint32_t magic; // +0: 'RsLi' (0x694C7352)
|
||||||
|
int32_t entry_count; // +4: Количество записей
|
||||||
|
uint32_t media_offset; // +8: Смещение медиа-оверлея
|
||||||
|
uint32_t reserved_0C; // +12: 0
|
||||||
|
HANDLE file_handle_2; // +16: -1 (дополнительный хэндл)
|
||||||
|
uint32_t reserved_14; // +20: 0
|
||||||
|
uint32_t reserved_18; // +24: —
|
||||||
|
uint32_t reserved_1C; // +28: 0
|
||||||
|
HANDLE mapping_handle_2; // +32: -1
|
||||||
|
uint32_t reserved_24; // +36: 0
|
||||||
|
uint32_t flag_28; // +40: (flags >> 7) & 1
|
||||||
|
HANDLE file_handle; // +44: Хэндл файла
|
||||||
|
HANDLE mapping_handle; // +48: Хэндл файлового маппинга
|
||||||
|
void* mapped_view; // +52: Указатель на mapped view
|
||||||
|
};
|
||||||
|
// Далее следуют entry_count записей по 64 байта каждая
|
||||||
|
```
|
||||||
|
|
||||||
|
### Внутренняя запись RsLi (64 байта)
|
||||||
|
|
||||||
|
```c
|
||||||
|
struct RsLibEntry { // 64 байта (16 DWORD)
|
||||||
|
char name[16]; // +0: Имя (12 из файла + 4 нуля)
|
||||||
|
int32_t flags; // +16: Флаги (sign-extended из int16)
|
||||||
|
int32_t sort_index; // +20: sort_to_original[i] (таблица индексов / XOR‑ключ)
|
||||||
|
uint32_t uncompressed_size; // +24: Размер несжатых данных (из поля 20 записи)
|
||||||
|
void* data_ptr; // +28: Указатель на данные в mapped view
|
||||||
|
uint32_t compressed_size; // +32: Размер сжатых данных (из поля 28 записи)
|
||||||
|
uint32_t reserved_24; // +36: 0
|
||||||
|
uint32_t reserved_28; // +40: 0
|
||||||
|
uint32_t reserved_2C; // +44: 0
|
||||||
|
void* loaded_data; // +48: Указатель на декомпрессированные данные
|
||||||
|
// +52..+63: дополнительные поля
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Часть 5. Экспортируемые API-функции
|
||||||
|
|
||||||
|
## 5.1. NRes API
|
||||||
|
|
||||||
|
| Функция | Описание |
|
||||||
|
| ------------------------------ | ------------------------------------------------------------------------- |
|
||||||
|
| `niOpenResFile(path)` | Открыть NRes-архив (только чтение), эквивалент `niOpenResFileEx(path, 0)` |
|
||||||
|
| `niOpenResFileEx(path, flags)` | Открыть NRes-архив с флагами |
|
||||||
|
| `niOpenResInMem(ptr, size)` | Открыть NRes-архив из памяти |
|
||||||
|
| `niCreateResFile(path)` | Создать/открыть NRes-архив для записи |
|
||||||
|
|
||||||
|
## 5.2. RsLi API
|
||||||
|
|
||||||
|
| Функция | Описание |
|
||||||
|
| ------------------------------- | -------------------------------------------------------- |
|
||||||
|
| `rsOpenLib(path, flags)` | Открыть RsLi-библиотеку |
|
||||||
|
| `rsCloseLib(lib)` | Закрыть библиотеку |
|
||||||
|
| `rsLibNum(lib)` | Получить количество записей |
|
||||||
|
| `rsFind(lib, name)` | Найти запись по имени (→ индекс или −1) |
|
||||||
|
| `rsLoad(lib, index)` | Загрузить и декомпрессировать ресурс |
|
||||||
|
| `rsLoadFast(lib, index, flags)` | Быстрая загрузка (без декомпрессии если возможно) |
|
||||||
|
| `rsLoadPacked(lib, index)` | Загрузить в «упакованном» виде (отложенная декомпрессия) |
|
||||||
|
| `rsLoadByName(lib, name)` | `rsFind` + `rsLoad` |
|
||||||
|
| `rsGetInfo(lib, index, out)` | Получить имя и размер ресурса |
|
||||||
|
| `rsGetPackMethod(lib, index)` | Получить метод сжатия (`flags & 0x1C0`) |
|
||||||
|
| `ngiUnpack(packed)` | Декомпрессировать ранее загруженный упакованный ресурс |
|
||||||
|
| `ngiAlloc(size)` | Выделить память (с обработкой ошибок) |
|
||||||
|
| `ngiFree(ptr)` | Освободить память |
|
||||||
|
| `ngiGetMemSize(ptr)` | Получить размер выделенного блока |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Часть 6. Контрольные заметки для реализации
|
||||||
|
|
||||||
|
## 6.1. Кодировки и регистр
|
||||||
|
|
||||||
|
- **NRes**: имена хранятся **как есть** (case-insensitive при поиске через `_strcmpi`).
|
||||||
|
- **RsLi**: имена хранятся в **верхнем регистре**. Перед поиском запрос приводится к верхнему регистру (`_strupr`). Сравнение — через `strcmp` (case-sensitive для уже uppercase строк).
|
||||||
|
|
||||||
|
## 6.2. Порядок байт
|
||||||
|
|
||||||
|
Все значения хранятся в **little-endian** порядке (платформа x86/Win32).
|
||||||
|
|
||||||
|
## 6.3. Выравнивание
|
||||||
|
|
||||||
|
- **NRes**: данные каждого ресурса выровнены по границе **8 байт** (0-padding между файлами).
|
||||||
|
- **RsLi**: выравнивание данных не описано в коде (данные идут подряд).
|
||||||
|
|
||||||
|
## 6.4. Размер записей на диске
|
||||||
|
|
||||||
|
- **NRes**: каталог — **64 байта** на запись, расположен в конце файла.
|
||||||
|
- **RsLi**: таблица — **32 байта** на запись (зашифрованная), расположена в начале файла (сразу после 32-байтного заголовка).
|
||||||
|
|
||||||
|
## 6.5. Кэширование и memory mapping
|
||||||
|
|
||||||
|
Оба формата используют Windows Memory-Mapped Files (`CreateFileMapping` + `MapViewOfFile`). NRes-архивы организованы в глобальный **связный список** (`dword_1003A66C`) со счётчиком ссылок и таймером неактивности (10 секунд = 0x2710 мс). При refcount == 0 и истечении таймера архив автоматически выгружается (если не установлен флаг `is_cacheable`).
|
||||||
|
|
||||||
|
## 6.6. Размер seed XOR
|
||||||
|
|
||||||
|
- **Заголовок RsLi**: seed — **4 байта** (DWORD) по смещению 20, но используются только младшие 2 байта (`lo = byte[0]`, `hi = byte[1]`).
|
||||||
|
- **Запись RsLi**: sort_to_original[i] — **2 байта** (int16) по смещению 18 записи.
|
||||||
|
- **Данные при комбинированном XOR+LZSS**: seed — **4 байта** (DWORD) из поля по смещению 20 записи, но опять используются только 2 байта.
|
||||||
@@ -23,11 +23,7 @@ theme:
|
|||||||
nav:
|
nav:
|
||||||
- Home: index.md
|
- Home: index.md
|
||||||
- Specs:
|
- Specs:
|
||||||
- Assets:
|
- NRes / RsLi: specs/nres.md
|
||||||
- NRes:
|
|
||||||
- Документация по формату: specs/assets/nres/overview.md
|
|
||||||
- FRES Декомпрессия: specs/assets/nres/fres_decompression.md
|
|
||||||
- Huffman Декомпрессия: specs/assets/nres/huffman_decompression.md
|
|
||||||
|
|
||||||
# Additional configuration
|
# Additional configuration
|
||||||
extra:
|
extra:
|
||||||
|
|||||||
Reference in New Issue
Block a user