feat: обновление документации по алгоритмам декомпрессии и добавление файлов .gitkeep в директории libs и tools
This commit is contained in:
@@ -51,37 +51,34 @@ dword_1003E0A4 // Длина повтора для LZ77
|
|||||||
Цикл декомпрессии:
|
Цикл декомпрессии:
|
||||||
Пока есть входные данные и выходной буфер не заполнен:
|
Пока есть входные данные и выходной буфер не заполнен:
|
||||||
|
|
||||||
1. Прочитать бит флага:
|
1. Прочитать бит флага (LSB-first):
|
||||||
if (flagBits высокий бит == 0):
|
if (flagBits == 0):
|
||||||
flags = *input++
|
flags = *input++
|
||||||
flagBits = 127 (0x7F)
|
flagBits = 8
|
||||||
|
|
||||||
flag_bit = flags & 1
|
flag_bit = flags & 1
|
||||||
flags >>= 1
|
flags >>= 1
|
||||||
|
flagBits -= 1
|
||||||
|
|
||||||
2. Прочитать второй бит:
|
2. Выбор действия по биту:
|
||||||
if (flagBits низкий бит == 0):
|
|
||||||
загрузить новый байт флагов
|
|
||||||
|
|
||||||
second_bit = flags & 1
|
a) Если bit == 1:
|
||||||
flags >>= 1
|
|
||||||
|
|
||||||
3. Выбор действия по битам:
|
|
||||||
|
|
||||||
a) Если оба бита == 0:
|
|
||||||
// Литерал - копировать один байт
|
// Литерал - копировать один байт
|
||||||
byte = *input++
|
byte = *input++
|
||||||
window[position] = byte
|
window[position] = byte
|
||||||
*output++ = byte
|
*output++ = byte
|
||||||
position = (position + 1) & 0xFFF
|
position = (position + 1) & 0xFFF
|
||||||
|
|
||||||
b) Если второй бит == 0 (первый == 1):
|
b) Если bit == 0:
|
||||||
// LZ77 копирование
|
// LZ77 копирование (2 байта)
|
||||||
word = *(uint16*)input
|
word = *(uint16*)input
|
||||||
input += 2
|
input += 2
|
||||||
|
|
||||||
offset = (word >> 4) & 0xFFF // 12 бит offset
|
b0 = word & 0xFF
|
||||||
length = (word & 0xF) + 3 // 4 бита длины + 3
|
b1 = (word >> 8) & 0xFF
|
||||||
|
|
||||||
|
offset = b0 | ((b1 & 0xF0) << 4) // 12 бит offset
|
||||||
|
length = (b1 & 0x0F) + 3 // 4 бита длины + 3
|
||||||
|
|
||||||
src_pos = offset
|
src_pos = offset
|
||||||
Повторить length раз:
|
Повторить length раз:
|
||||||
@@ -97,19 +94,20 @@ dword_1003E0A4 // Длина повтора для LZ77
|
|||||||
```
|
```
|
||||||
Битовый поток:
|
Битовый поток:
|
||||||
|
|
||||||
[FLAG_BIT] [SECOND_BIT] [DATA]
|
Битовый поток:
|
||||||
|
|
||||||
|
[FLAG_BIT] [DATA]
|
||||||
|
|
||||||
Где:
|
Где:
|
||||||
FLAG_BIT = 0, SECOND_BIT = 0: → Литерал (1 байт следует)
|
FLAG_BIT = 1: → Литерал (1 байт следует)
|
||||||
FLAG_BIT = 1, SECOND_BIT = 0: → LZ77 копирование (2 байта следуют)
|
FLAG_BIT = 0: → LZ77 копирование (2 байта следуют)
|
||||||
FLAG_BIT = любой, SECOND_BIT = 1: → Литерал (1 байт следует)
|
|
||||||
|
|
||||||
Формат LZ77 копирования (2 байта, little-endian):
|
Формат LZ77 копирования (2 байта, little-endian):
|
||||||
Байт 0: offset_low (биты 0-7)
|
Байт 0: offset_low (биты 0-7)
|
||||||
Байт 1: [length:4][offset_high:4]
|
Байт 1: [length:4][offset_high:4]
|
||||||
|
|
||||||
offset = (byte1 >> 4) | (byte0 << 4) // 12 бит
|
offset = byte0 | ((byte1 & 0xF0) << 4) // 12 бит
|
||||||
length = (byte1 & 0x0F) + 3 // 4 бита + 3 = 3-18 байт
|
length = (byte1 & 0x0F) + 3 // 4 бита + 3 = 3-18 байт
|
||||||
```
|
```
|
||||||
|
|
||||||
## Режим 2: Adaptive Huffman режим (a1 < 0)
|
## Режим 2: Adaptive Huffman режим (a1 < 0)
|
||||||
@@ -122,7 +120,8 @@ dword_1003E0A4 // Длина повтора для LZ77
|
|||||||
Инициализация таблиц:
|
Инициализация таблиц:
|
||||||
1. Создание таблицы быстрого декодирования (dword_1003B94C[256])
|
1. Создание таблицы быстрого декодирования (dword_1003B94C[256])
|
||||||
2. Инициализация длин кодов (byte_1003BD4C[256])
|
2. Инициализация длин кодов (byte_1003BD4C[256])
|
||||||
3. Построение начального дерева (627 узлов)
|
3. Построение начального дерева (627 узлов, T = 2*N_CHAR - 1)
|
||||||
|
где N_CHAR = 314 (256 литералов + 58 кодов длины)
|
||||||
```
|
```
|
||||||
|
|
||||||
### Алгоритм декодирования
|
### Алгоритм декодирования
|
||||||
@@ -161,14 +160,11 @@ dword_1003E0A4 // Длина повтора для LZ77
|
|||||||
position = (position + 1) & 0xFFF
|
position = (position + 1) & 0xFFF
|
||||||
|
|
||||||
else:
|
else:
|
||||||
// LZ77 копирование
|
// LZSS копирование (LZHUF)
|
||||||
length = symbol - 253
|
length = symbol - 253 // 3..60
|
||||||
|
match_pos = decode_position() // префикс + 6 бит
|
||||||
|
|
||||||
// Прочитать offset (закодирован отдельно)
|
src_pos = (position - 1 - match_pos) & 0xFFF
|
||||||
offset_bits = прочитать_биты(таблица длин)
|
|
||||||
offset = декодировать(offset_bits)
|
|
||||||
|
|
||||||
src_pos = (position - 1 - offset) & 0xFFF
|
|
||||||
|
|
||||||
Повторить length раз:
|
Повторить length раз:
|
||||||
byte = window[src_pos]
|
byte = window[src_pos]
|
||||||
@@ -207,69 +203,23 @@ def fres_decompress_simple(input_data, output_size):
|
|||||||
|
|
||||||
input_pos = 0
|
input_pos = 0
|
||||||
flags = 0
|
flags = 0
|
||||||
flag_bits_high = 0
|
flag_bits = 0
|
||||||
flag_bits_low = 0
|
|
||||||
|
|
||||||
while len(output) < output_size and input_pos < len(input_data):
|
while len(output) < output_size and input_pos < len(input_data):
|
||||||
# Читаем флаг высокого бита
|
# Читаем флаг (LSB-first)
|
||||||
if (flag_bits_high & 1) == 0:
|
if flag_bits == 0:
|
||||||
if input_pos >= len(input_data):
|
if input_pos >= len(input_data):
|
||||||
break
|
break
|
||||||
flags = input_data[input_pos]
|
flags = input_data[input_pos]
|
||||||
input_pos += 1
|
input_pos += 1
|
||||||
flag_bits_high = 127 # 0x7F
|
flag_bits = 8
|
||||||
|
|
||||||
flag_high = flag_bits_high & 1
|
flag = flags & 1
|
||||||
flag_bits_high >>= 1
|
|
||||||
|
|
||||||
# Читаем флаг низкого бита
|
|
||||||
if input_pos >= len(input_data):
|
|
||||||
break
|
|
||||||
|
|
||||||
if (flag_bits_low & 1) == 0:
|
|
||||||
flags = input_data[input_pos]
|
|
||||||
input_pos += 1
|
|
||||||
flag_bits_low = 127
|
|
||||||
|
|
||||||
flag_low = flags & 1
|
|
||||||
flags >>= 1
|
flags >>= 1
|
||||||
|
flag_bits -= 1
|
||||||
|
|
||||||
# Обработка по флагам
|
# Обработка по флагу
|
||||||
if not flag_low: # Второй бит == 0
|
if flag: # 1 = literal
|
||||||
if not flag_high: # Оба бита == 0
|
|
||||||
# Литерал
|
|
||||||
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: # Первый == 1, второй == 0
|
|
||||||
# LZ77 копирование
|
|
||||||
if input_pos + 1 >= len(input_data):
|
|
||||||
break
|
|
||||||
|
|
||||||
word = input_data[input_pos] | (input_data[input_pos + 1] << 8)
|
|
||||||
input_pos += 2
|
|
||||||
|
|
||||||
offset = (word >> 4) & 0xFFF
|
|
||||||
length = (word & 0xF) + 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
|
|
||||||
|
|
||||||
else: # Второй бит == 1
|
|
||||||
# Литерал
|
# Литерал
|
||||||
if input_pos >= len(input_data):
|
if input_pos >= len(input_data):
|
||||||
break
|
break
|
||||||
@@ -279,6 +229,27 @@ def fres_decompress_simple(input_data, output_size):
|
|||||||
window[position] = byte
|
window[position] = byte
|
||||||
output.append(byte)
|
output.append(byte)
|
||||||
position = (position + 1) & 0xFFF
|
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])
|
return bytes(output[:output_size])
|
||||||
```
|
```
|
||||||
@@ -347,7 +318,10 @@ def initialize_window():
|
|||||||
|
|
||||||
### 4. Битовые флаги
|
### 4. Битовые флаги
|
||||||
|
|
||||||
Используется двойная система флагов для определения типа следующих данных
|
Используется один флаговый бит (LSB-first) для определения типа данных:
|
||||||
|
|
||||||
|
- `1` → literal (1 байт)
|
||||||
|
- `0` → backref (2 байта)
|
||||||
|
|
||||||
## Проблемы реализации
|
## Проблемы реализации
|
||||||
|
|
||||||
@@ -363,6 +337,8 @@ def initialize_window():
|
|||||||
|
|
||||||
Необходимо тщательно проверять границы буферов
|
Необходимо тщательно проверять границы буферов
|
||||||
|
|
||||||
|
- В простом режиме перед backref нужно гарантировать наличие **2 байт** входных данных
|
||||||
|
|
||||||
## Примеры данных
|
## Примеры данных
|
||||||
|
|
||||||
### Пример 1: Литералы (простой режим)
|
### Пример 1: Литералы (простой режим)
|
||||||
@@ -372,7 +348,7 @@ def initialize_window():
|
|||||||
Выход: Последовательность литералов
|
Выход: Последовательность литералов
|
||||||
|
|
||||||
Пример:
|
Пример:
|
||||||
Flags: 0x00 (00000000)
|
Flags: 0xFF (11111111)
|
||||||
Data: 0x41 ('A'), 0x42 ('B'), 0x43 ('C'), ...
|
Data: 0x41 ('A'), 0x42 ('B'), 0x43 ('C'), ...
|
||||||
Выход: "ABC..."
|
Выход: "ABC..."
|
||||||
```
|
```
|
||||||
@@ -384,12 +360,12 @@ def initialize_window():
|
|||||||
Выход: Копирование из окна
|
Выход: Копирование из окна
|
||||||
|
|
||||||
Пример:
|
Пример:
|
||||||
Flags: 0x01 (00000001) - первый бит = 1
|
Flags: 0x00 (00000000) - первый бит = 0
|
||||||
Word: 0x1234
|
Bytes: b0=0x34, b1=0x12
|
||||||
|
|
||||||
Разбор:
|
Разбор:
|
||||||
offset = (0x34 << 4) | (0x12 >> 4) = 0x341
|
offset = 0x34 | ((0x12 & 0xF0) << 4) = 0x234
|
||||||
length = (0x12 & 0xF) + 3 = 5
|
length = (0x12 & 0x0F) + 3 = 5
|
||||||
|
|
||||||
Действие: Скопировать 5 байт с позиции offset
|
Действие: Скопировать 5 байт с позиции offset
|
||||||
```
|
```
|
||||||
@@ -406,7 +382,7 @@ def debug_fres_decompress(input_data, output_size):
|
|||||||
|
|
||||||
# ... реализация с print на каждом шаге
|
# ... реализация с print на каждом шаге
|
||||||
|
|
||||||
print(f"Flag: {flag_high}{flag_low}")
|
print(f"Flag: {flag}")
|
||||||
if is_literal:
|
if is_literal:
|
||||||
print(f" Literal: 0x{byte:02X}")
|
print(f" Literal: 0x{byte:02X}")
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
## Обзор
|
## Обзор
|
||||||
|
|
||||||
Это реализация **DEFLATE-подобного** алгоритма декомпрессии, используемого в [NRes](overview.md). Алгоритм поддерживает три режима блоков и использует два Huffman дерева для кодирования литералов/длин и расстояний.
|
Это реализация **RAW-DEFLATE (inflate)**, используемого в [NRes](overview.md). Поток подаётся без zlib-обёртки (нет 2-байтового заголовка и Adler32). Алгоритм поддерживает три режима блоков и использует два Huffman дерева для кодирования литералов/длин и расстояний.
|
||||||
|
|
||||||
```c
|
```c
|
||||||
int __thiscall sub_1001AF10(
|
int __thiscall sub_1001AF10(
|
||||||
@@ -15,30 +15,31 @@ int __thiscall sub_1001AF10(
|
|||||||
|
|
||||||
```c
|
```c
|
||||||
struct HuffmanContext {
|
struct HuffmanContext {
|
||||||
uint32_t bitBuffer[0x4000]; // 0x00000-0x0FFFF: Битовый буфер (32KB)
|
uint8_t window[0x10000]; // 0x00000-0x0FFFF: Внутренний буфер/окно
|
||||||
uint32_t compressedSize; // 0x10000: Размер сжатых данных
|
uint32_t compressedSize; // 0x10000: packedSize
|
||||||
uint32_t unknown1; // 0x10004: Не используется
|
uint32_t outputPosition; // 0x10004: Сколько уже выведено
|
||||||
uint32_t outputPosition; // 0x10008: Позиция в выходном буфере
|
uint32_t windowPos; // 0x10008: Позиция в 0x8000 окне
|
||||||
uint32_t currentByte; // 0x1000C: Текущий байт
|
uint32_t sourcePtr; // 0x1000C: Указатель на сжатые данные
|
||||||
uint8_t* sourceData; // 0x10010: Указатель на сжатые данные
|
uint32_t destPtr; // 0x10010: Указатель на выходной буфер
|
||||||
uint8_t* destData; // 0x10014: Указатель на выходной буфер
|
uint32_t sourcePos; // 0x10014: Текущая позиция чтения
|
||||||
uint32_t bitPosition; // 0x10018: Позиция бита
|
uint32_t unpackedSize; // 0x10018: Ожидаемый размер распаковки
|
||||||
uint32_t inputPosition; // 0x1001C: Позиция чтения (this[16389])
|
uint32_t bitBufferValue; // 0x1001C: Битовый буфер
|
||||||
uint32_t decodedBytes; // 0x10020: Декодированные байты (this[16386])
|
uint32_t bitsAvailable; // 0x10020: Количество доступных бит
|
||||||
uint32_t bitBufferValue; // 0x10024: Значение бит буфера (this[16391])
|
uint32_t maxWindowPosSeen; // 0x10024: Максимум окна (статистика)
|
||||||
uint32_t bitsAvailable; // 0x10028: Доступные биты (this[16392])
|
|
||||||
// ...
|
// ...
|
||||||
};
|
};
|
||||||
|
|
||||||
// Смещения в структуре:
|
// Смещения в структуре (индексация this[]):
|
||||||
#define CTX_OUTPUT_POS 16385 // this[16385]
|
#define CTX_COMPRESSED_SIZE 0x4000 // this[0x4000] == 0x10000
|
||||||
#define CTX_DECODED_BYTES 16386 // this[16386]
|
#define CTX_OUTPUT_POS 16385 // this[16385] == 0x10004
|
||||||
#define CTX_SOURCE_PTR 16387 // this[16387]
|
#define CTX_WINDOW_POS 16386 // this[16386] == 0x10008
|
||||||
#define CTX_DEST_PTR 16388 // this[16388]
|
#define CTX_SOURCE_PTR 16387 // this[16387] == 0x1000C
|
||||||
#define CTX_INPUT_POS 16389 // this[16389]
|
#define CTX_DEST_PTR 16388 // this[16388] == 0x10010
|
||||||
#define CTX_BIT_BUFFER 16391 // this[16391]
|
#define CTX_SOURCE_POS 16389 // this[16389] == 0x10014
|
||||||
#define CTX_BITS_COUNT 16392 // this[16392]
|
#define CTX_UNPACKED_SIZE 16390 // this[16390] == 0x10018
|
||||||
#define CTX_MAX_SYMBOL 16393 // this[16393]
|
#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
|
||||||
```
|
```
|
||||||
|
|
||||||
## Три режима блоков
|
## Три режима блоков
|
||||||
@@ -56,6 +57,12 @@ TYPE:
|
|||||||
11 = Зарезервировано (ошибка)
|
11 = Зарезервировано (ошибка)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Соответствие функциям:
|
||||||
|
|
||||||
|
- type 0 → `sub_1001A750` (stored)
|
||||||
|
- type 1 → `sub_1001A8C0` (fixed Huffman)
|
||||||
|
- type 2 → `sub_1001AA30` (dynamic Huffman)
|
||||||
|
|
||||||
### Основной цикл декодирования
|
### Основной цикл декодирования
|
||||||
|
|
||||||
```c
|
```c
|
||||||
@@ -417,33 +424,33 @@ def decode_huffman_symbol(ctx, tree):
|
|||||||
```python
|
```python
|
||||||
def write_output_byte(ctx, byte):
|
def write_output_byte(ctx, byte):
|
||||||
"""Записать байт в выходной буфер"""
|
"""Записать байт в выходной буфер"""
|
||||||
# Записываем в bitBuffer (используется как циклический буфер)
|
# Записываем в окно 0x8000
|
||||||
ctx.bitBuffer[ctx.decodedBytes] = byte
|
ctx.window[ctx.windowPos] = byte
|
||||||
ctx.decodedBytes += 1
|
ctx.windowPos += 1
|
||||||
|
|
||||||
# Если буфер заполнен (32KB)
|
# Если окно заполнено (32KB)
|
||||||
if ctx.decodedBytes >= 0x8000:
|
if ctx.windowPos >= 0x8000:
|
||||||
flush_output_buffer(ctx)
|
flush_output_buffer(ctx)
|
||||||
|
|
||||||
|
|
||||||
def flush_output_buffer(ctx):
|
def flush_output_buffer(ctx):
|
||||||
"""Сбросить выходной буфер в финальный выход"""
|
"""Сбросить выходной буфер в финальный выход"""
|
||||||
# Копируем данные в финальный выходной буфер
|
# Копируем окно в финальный выходной буфер
|
||||||
dest_offset = ctx.outputPosition + ctx.destData
|
dest_offset = ctx.outputPosition + ctx.destPtr
|
||||||
memcpy(dest_offset, ctx.bitBuffer, ctx.decodedBytes)
|
memcpy(dest_offset, ctx.window, ctx.windowPos)
|
||||||
|
|
||||||
# Обновляем счетчики
|
# Обновляем счетчики
|
||||||
ctx.outputPosition += ctx.decodedBytes
|
ctx.outputPosition += ctx.windowPos
|
||||||
ctx.decodedBytes = 0
|
ctx.windowPos = 0
|
||||||
|
|
||||||
|
|
||||||
def copy_from_history(ctx, distance, length):
|
def copy_from_history(ctx, distance, length):
|
||||||
"""Скопировать данные из истории (LZ77)"""
|
"""Скопировать данные из истории (LZ77)"""
|
||||||
# Позиция источника в циклическом буфере
|
# Позиция источника в циклическом буфере
|
||||||
src_pos = (ctx.decodedBytes - distance) & 0x7FFF
|
src_pos = (ctx.windowPos - distance) & 0x7FFF
|
||||||
|
|
||||||
for i in range(length):
|
for i in range(length):
|
||||||
byte = ctx.bitBuffer[src_pos]
|
byte = ctx.window[src_pos]
|
||||||
write_output_byte(ctx, byte)
|
write_output_byte(ctx, byte)
|
||||||
src_pos = (src_pos + 1) & 0x7FFF
|
src_pos = (src_pos + 1) & 0x7FFF
|
||||||
```
|
```
|
||||||
@@ -452,7 +459,7 @@ def copy_from_history(ctx, distance, length):
|
|||||||
|
|
||||||
```python
|
```python
|
||||||
class HuffmanDecoder:
|
class HuffmanDecoder:
|
||||||
"""Полный DEFLATE-подобный декодер"""
|
"""Полный RAW-DEFLATE декодер"""
|
||||||
|
|
||||||
def __init__(self, input_data, output_size):
|
def __init__(self, input_data, output_size):
|
||||||
self.input_data = input_data
|
self.input_data = input_data
|
||||||
@@ -582,7 +589,7 @@ def debug_huffman_decode(data):
|
|||||||
|
|
||||||
## Заключение
|
## Заключение
|
||||||
|
|
||||||
Этот Huffman декодер реализует **DEFLATE**-совместимый алгоритм с тремя режимами блоков:
|
Этот декодер реализует **RAW-DEFLATE** с тремя режимами блоков:
|
||||||
|
|
||||||
1. **Несжатый** - для несжимаемых данных
|
1. **Несжатый** - для несжимаемых данных
|
||||||
2. **Фиксированный Huffman** - быстрое декодирование с предопределенными таблицами
|
2. **Фиксированный Huffman** - быстрое декодирование с предопределенными таблицами
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ NRes — это формат контейнера ресурсов, исполь
|
|||||||
|
|
||||||
```c
|
```c
|
||||||
struct NResHeader {
|
struct NResHeader {
|
||||||
uint32_t signature; // +0x00: Сигнатура "NRes" (0x7365526E в little-endian)
|
uint32_t signature; // +0x00: Сигнатура "NRes" (0x7365524E в little-endian)
|
||||||
uint32_t version; // +0x04: Версия формата (0x00000100 = версия 1.0)
|
uint32_t version; // +0x04: Версия формата (0x00000100 = версия 1.0)
|
||||||
uint32_t fileCount; // +0x08: Количество файлов в архиве
|
uint32_t fileCount; // +0x08: Количество файлов в архиве
|
||||||
uint32_t fileSize; // +0x0C: Общий размер файла в байтах
|
uint32_t fileSize; // +0x0C: Общий размер файла в байтах
|
||||||
@@ -19,7 +19,7 @@ struct NResHeader {
|
|||||||
|
|
||||||
**Детали:**
|
**Детали:**
|
||||||
|
|
||||||
- `signature`: Константа `0x7365526E` (1936020046 в десятичном виде). Это ASCII строка "nRes" в обратном порядке байт
|
- `signature`: Константа `0x7365524E` (1936020046 в десятичном виде). Это ASCII строка "NRes" в обратном порядке байт
|
||||||
- `version`: Всегда должна быть `0x00000100` (256 в десятичном виде) для версии 1.0
|
- `version`: Всегда должна быть `0x00000100` (256 в десятичном виде) для версии 1.0
|
||||||
- `fileCount`: Общее количество файлов в архиве (используется для валидации)
|
- `fileCount`: Общее количество файлов в архиве (используется для валидации)
|
||||||
- `fileSize`: Полный размер NRes файла, включая заголовок
|
- `fileSize`: Полный размер NRes файла, включая заголовок
|
||||||
@@ -65,7 +65,7 @@ DirectoryOffset = FileSize - (FileCount * 64)
|
|||||||
struct NResFileEntry {
|
struct NResFileEntry {
|
||||||
char name[16]; // +0x00: Имя файла (NULL-terminated, uppercase)
|
char name[16]; // +0x00: Имя файла (NULL-terminated, uppercase)
|
||||||
uint32_t crc32; // +0x10: CRC32 хеш упакованных данных
|
uint32_t crc32; // +0x10: CRC32 хеш упакованных данных
|
||||||
uint32_t packMethod; // +0x14: Флаги метода упаковки и опции
|
uint32_t packMethod; // +0x14: Флаги метода упаковки (также используется как XOR seed)
|
||||||
uint32_t unpackedSize; // +0x18: Размер файла после распаковки
|
uint32_t unpackedSize; // +0x18: Размер файла после распаковки
|
||||||
uint32_t packedSize; // +0x1C: Размер упакованных данных
|
uint32_t packedSize; // +0x1C: Размер упакованных данных
|
||||||
uint32_t dataOffset; // +0x20: Смещение данных от начала файла
|
uint32_t dataOffset; // +0x20: Смещение данных от начала файла
|
||||||
@@ -98,20 +98,17 @@ struct NResFileEntry {
|
|||||||
|
|
||||||
```c
|
```c
|
||||||
// Маски для извлечения метода упаковки
|
// Маски для извлечения метода упаковки
|
||||||
#define PACK_METHOD_MASK 0x1E0 // Биты 5-8 (основной метод)
|
#define PACK_METHOD_MASK 0x1E0 // Биты 5-8 (метод + XOR)
|
||||||
#define PACK_METHOD_MASK2 0x1C0 // Биты 6-7 (альтернативная маска)
|
#define PACK_METHOD_MASK2 0x1C0 // Биты 6-7 (без XOR-бита)
|
||||||
|
|
||||||
// Методы упаковки (биты 5-8)
|
// Методы упаковки (packMethod & 0x1E0)
|
||||||
#define PACK_NONE 0x000 // Нет упаковки (копирование)
|
#define PACK_NONE 0x000 // Нет упаковки (raw)
|
||||||
#define PACK_XOR 0x020 // XOR-шифрование
|
#define PACK_XOR 0x020 // XOR (только шифрование)
|
||||||
#define PACK_FRES 0x040 // FRES компрессия (устаревшая)
|
#define PACK_FRES 0x040 // FRES (LZSS простой режим)
|
||||||
#define PACK_FRES_XOR 0x060 // FRES + XOR (два прохода)
|
#define PACK_FRES_XOR 0x060 // XOR + FRES
|
||||||
#define PACK_ZLIB 0x080 // Zlib сжатие (устаревшее)
|
#define PACK_LZHUF 0x080 // LZHUF (LZSS + adaptive Huffman)
|
||||||
#define PACK_ZLIB_XOR 0x0A0 // Zlib + XOR (два прохода)
|
#define PACK_LZHUF_XOR 0x0A0 // XOR + LZHUF
|
||||||
#define PACK_HUFFMAN 0x0E0 // Huffman кодирование (основной метод)
|
#define PACK_DEFLATE_RAW 0x100 // RAW-DEFLATE (без zlib-обёртки)
|
||||||
|
|
||||||
// Дополнительные флаги
|
|
||||||
#define FLAG_ENCRYPTED 0x040 // Файл зашифрован/требует декодирования
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**Алгоритм определения метода:**
|
**Алгоритм определения метода:**
|
||||||
@@ -120,9 +117,13 @@ struct NResFileEntry {
|
|||||||
2. Проверить конкретные значения:
|
2. Проверить конкретные значения:
|
||||||
- `0x000`: Данные не сжаты, простое копирование
|
- `0x000`: Данные не сжаты, простое копирование
|
||||||
- `0x020`: XOR-шифрование с двухбайтовым ключом
|
- `0x020`: XOR-шифрование с двухбайтовым ключом
|
||||||
- `0x040` или `0x060`: FRES компрессия (может быть + XOR)
|
- `0x040` или `0x060`: FRES (может быть + XOR)
|
||||||
- `0x080` или `0x0A0`: Zlib компрессия (может быть + XOR)
|
- `0x080` или `0x0A0`: LZHUF (может быть + XOR)
|
||||||
- `0x0E0`: Huffman кодирование (наиболее распространенный)
|
- `0x100`: RAW-DEFLATE (inflate без zlib-обёртки)
|
||||||
|
|
||||||
|
**Важно:** `rsGetPackMethod()` возвращает `packMethod & 0x1C0`, то есть маску **без XOR-бита `0x20`**. Это нужно учитывать при сравнении.
|
||||||
|
|
||||||
|
**Примечание про XOR seed:** значение для XOR берётся из поля `packMethod` (смещение `+0x14`). Это же поле может быть перезаписано при формировании каталога (см. раздел о `rsOpenLib`), если в библиотеке нет готовой таблицы сортировки.
|
||||||
|
|
||||||
### Поле: unpackedSize (смещение +0x18, 4 байта)
|
### Поле: unpackedSize (смещение +0x18, 4 байта)
|
||||||
|
|
||||||
@@ -163,9 +164,9 @@ struct NResFileEntry {
|
|||||||
|
|
||||||
- **Назначение**: Индекс для быстрого поиска по отсортированному каталогу
|
- **Назначение**: Индекс для быстрого поиска по отсортированному каталогу
|
||||||
- **Использование**:
|
- **Использование**:
|
||||||
- Каталог сортируется по алфавиту (имени файлов)
|
- В `rsOpenLib` при отсутствии маркера `0xABBA` формируется таблица индексов сортировки имён
|
||||||
- `sortIndex` хранит оригинальный порядковый номер файла
|
- Индексы записываются в это поле с шагом 0x40 (по записи)
|
||||||
- Позволяет использовать бинарный поиск для функции `rsFind()`
|
- Используется `rsFind()` через таблицу индексов, а не прямую сортировку записей
|
||||||
|
|
||||||
### Поле: reserved (смещение +0x30, 16 байт)
|
### Поле: reserved (смещение +0x30, 16 байт)
|
||||||
|
|
||||||
@@ -185,8 +186,8 @@ struct NResFileEntry {
|
|||||||
### 2. XOR-шифрование (PACK_XOR = 0x020)
|
### 2. XOR-шифрование (PACK_XOR = 0x020)
|
||||||
|
|
||||||
```c
|
```c
|
||||||
// Ключ берется из поля crc32
|
// Ключ/seed берется из поля packMethod (смещение +0x14)
|
||||||
uint16_t key = (uint16_t)(crc32 & 0xFFFF);
|
uint16_t key = (uint16_t)(packMethod & 0xFFFF);
|
||||||
|
|
||||||
for (int i = 0; i < packedSize; i++) {
|
for (int i = 0; i < packedSize; i++) {
|
||||||
uint8_t byte = source[i];
|
uint8_t byte = source[i];
|
||||||
@@ -200,7 +201,7 @@ for (int i = 0; i < packedSize; i++) {
|
|||||||
|
|
||||||
**Ключевые особенности:**
|
**Ключевые особенности:**
|
||||||
|
|
||||||
- Используется 16-битный ключ из младших байт CRC32
|
- Используется 16-битный ключ из младших байт поля `packMethod`
|
||||||
- Ключ изменяется после каждого байта по специальному алгоритму
|
- Ключ изменяется после каждого байта по специальному алгоритму
|
||||||
- Операции: XOR с старшим байтом ключа и со сдвинутым значением
|
- Операции: XOR с старшим байтом ключа и со сдвинутым значением
|
||||||
|
|
||||||
@@ -215,30 +216,19 @@ sub_1001B22E() - функция декомпрессии FRES
|
|||||||
- Использует скользящее окно для ссылок
|
- Использует скользящее окно для ссылок
|
||||||
```
|
```
|
||||||
|
|
||||||
### 4. [Huffman кодирование](huffman_decompression.md) (PACK_HUFFMAN = 0x0E0)
|
### 4. [LZHUF (adaptive Huffman)](fres_decompression.md) (PACK_LZHUF = 0x080, 0x0A0)
|
||||||
|
|
||||||
Наиболее сложный и эффективный метод:
|
Наиболее сложный и эффективный метод:
|
||||||
|
|
||||||
```c
|
|
||||||
// Структура декодера
|
|
||||||
struct HuffmanDecoder {
|
|
||||||
uint32_t bitBuffer[0x4000]; // Буфер для битов
|
|
||||||
uint32_t compressedSize; // Размер сжатых данных
|
|
||||||
uint32_t outputPosition; // Текущая позиция в выходном буфере
|
|
||||||
uint32_t inputPosition; // Позиция в входных данных
|
|
||||||
uint8_t* sourceData; // Указатель на сжатые данные
|
|
||||||
uint8_t* destData; // Указатель на выходной буфер
|
|
||||||
uint32_t bitPosition; // Позиция бита в буфере
|
|
||||||
// ... дополнительные поля
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
**Процесс декодирования:**
|
**Процесс декодирования:**
|
||||||
|
|
||||||
1. Инициализация структуры декодера
|
1. Распаковка LZSS + adaptive Huffman (Okumura LZHUF)
|
||||||
2. Чтение битов и построение дерева Huffman
|
2. Дерево обновляется после каждого символа
|
||||||
3. Декодирование символов по дереву
|
3. Match-символы преобразуются в длину и позицию
|
||||||
4. Запись в выходной буфер
|
|
||||||
|
### 5. [RAW-DEFLATE](huffman_decompression.md) (PACK_DEFLATE_RAW = 0x100)
|
||||||
|
|
||||||
|
Это inflate без zlib-обёртки (без 2-байтового заголовка и Adler32).
|
||||||
|
|
||||||
## Высокоуровневая инструкция по реализации
|
## Высокоуровневая инструкция по реализации
|
||||||
|
|
||||||
@@ -252,7 +242,7 @@ def open_nres_file(filepath):
|
|||||||
signature, version, file_count, file_size = struct.unpack('<4I', header_data)
|
signature, version, file_count, file_size = struct.unpack('<4I', header_data)
|
||||||
|
|
||||||
# 2. Проверяем сигнатуру
|
# 2. Проверяем сигнатуру
|
||||||
if signature != 0x7365526E: # "nRes"
|
if signature != 0x7365524E: # "NRes"
|
||||||
raise ValueError("Неверная сигнатура файла")
|
raise ValueError("Неверная сигнатура файла")
|
||||||
|
|
||||||
# 3. Проверяем версию
|
# 3. Проверяем версию
|
||||||
@@ -315,7 +305,7 @@ def read_directory(nres_file):
|
|||||||
```python
|
```python
|
||||||
def find_file(entries, filename):
|
def find_file(entries, filename):
|
||||||
# Имена в архиве хранятся в UPPERCASE
|
# Имена в архиве хранятся в UPPERCASE
|
||||||
search_name = filename.upper()
|
search_name = filename.upper()[:15]
|
||||||
|
|
||||||
# Используем бинарный поиск, так как каталог отсортирован
|
# Используем бинарный поиск, так как каталог отсортирован
|
||||||
# Сортировка по sort_index восстанавливает алфавитный порядок
|
# Сортировка по sort_index восстанавливает алфавитный порядок
|
||||||
@@ -357,20 +347,27 @@ def extract_file(nres_file, entry):
|
|||||||
|
|
||||||
elif pack_method == 0x020:
|
elif pack_method == 0x020:
|
||||||
# XOR-шифрование
|
# XOR-шифрование
|
||||||
return unpack_xor(packed_data, entry['crc32'], entry['unpacked_size'])
|
return unpack_xor(packed_data, entry['pack_method'], entry['unpacked_size'])
|
||||||
|
|
||||||
elif pack_method == 0x040 or pack_method == 0x060:
|
elif pack_method == 0x040 or pack_method == 0x060:
|
||||||
# FRES компрессия (может быть с XOR)
|
# FRES компрессия (может быть с XOR)
|
||||||
if pack_method == 0x060:
|
if pack_method == 0x060:
|
||||||
# Сначала XOR
|
# Сначала XOR
|
||||||
temp_data = unpack_xor(packed_data, entry['crc32'], entry['xor_size'])
|
temp_data = unpack_xor(packed_data, entry['pack_method'], entry['xor_size'])
|
||||||
return unpack_fres(temp_data, entry['unpacked_size'])
|
return unpack_fres(temp_data, entry['unpacked_size'])
|
||||||
else:
|
else:
|
||||||
return unpack_fres(packed_data, entry['unpacked_size'])
|
return unpack_fres(packed_data, entry['unpacked_size'])
|
||||||
|
|
||||||
elif pack_method == 0x0E0:
|
elif pack_method == 0x080 or pack_method == 0x0A0:
|
||||||
# Huffman кодирование
|
# LZHUF (может быть с XOR)
|
||||||
return unpack_huffman(packed_data, entry['unpacked_size'])
|
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:
|
else:
|
||||||
raise ValueError(f"Неподдерживаемый метод упаковки: 0x{pack_method:X}")
|
raise ValueError(f"Неподдерживаемый метод упаковки: 0x{pack_method:X}")
|
||||||
@@ -383,10 +380,10 @@ def unpack_none(data):
|
|||||||
"""Без упаковки - просто возвращаем данные"""
|
"""Без упаковки - просто возвращаем данные"""
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def unpack_xor(data, crc32, size):
|
def unpack_xor(data, pack_method, size):
|
||||||
"""XOR-дешифрование с изменяющимся ключом"""
|
"""XOR-дешифрование с изменяющимся ключом"""
|
||||||
result = bytearray(size)
|
result = bytearray(size)
|
||||||
key = crc32 & 0xFFFF # Берем младшие 16 бит
|
key = pack_method & 0xFFFF # Берем младшие 16 бит из поля packMethod
|
||||||
|
|
||||||
for i in range(min(size, len(data))):
|
for i in range(min(size, len(data))):
|
||||||
byte = data[i]
|
byte = data[i]
|
||||||
@@ -412,13 +409,22 @@ def unpack_fres(data, unpacked_size):
|
|||||||
decoder = FRESDecoder()
|
decoder = FRESDecoder()
|
||||||
return decoder.decompress(data, unpacked_size)
|
return decoder.decompress(data, unpacked_size)
|
||||||
|
|
||||||
def unpack_huffman(data, unpacked_size):
|
def unpack_lzhuf(data, unpacked_size):
|
||||||
"""
|
"""
|
||||||
Huffman декодирование (DEFLATE-подобный)
|
LZHUF (LZSS + adaptive Huffman)
|
||||||
Полная реализация в nres_decompression.py (класс HuffmanDecoder)
|
Полная реализация в nres_decompression.py (класс LZHUDecoder)
|
||||||
"""
|
"""
|
||||||
from nres_decompression import HuffmanDecoder
|
from nres_decompression import LZHUDecoder
|
||||||
decoder = HuffmanDecoder()
|
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)
|
return decoder.decompress(data, unpacked_size)
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -456,6 +462,30 @@ def extract_all(nres_filepath, output_dir):
|
|||||||
print(f" ✗ Ошибка: {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)
|
### 1. Порядок байт (Endianness)
|
||||||
@@ -539,7 +569,7 @@ def is_nres_file(filepath):
|
|||||||
try:
|
try:
|
||||||
with open(filepath, 'rb') as f:
|
with open(filepath, 'rb') as f:
|
||||||
signature = struct.unpack('<I', f.read(4))[0]
|
signature = struct.unpack('<I', f.read(4))[0]
|
||||||
return signature == 0x7365526E
|
return signature == 0x7365524E
|
||||||
except:
|
except:
|
||||||
return False
|
return False
|
||||||
```
|
```
|
||||||
@@ -553,9 +583,9 @@ def get_file_info(entry):
|
|||||||
0x020: "XOR",
|
0x020: "XOR",
|
||||||
0x040: "FRES",
|
0x040: "FRES",
|
||||||
0x060: "FRES+XOR",
|
0x060: "FRES+XOR",
|
||||||
0x080: "Zlib",
|
0x080: "LZHUF",
|
||||||
0x0A0: "Zlib+XOR",
|
0x0A0: "LZHUF+XOR",
|
||||||
0x0E0: "Huffman"
|
0x100: "RAW-DEFLATE"
|
||||||
}
|
}
|
||||||
|
|
||||||
pack_method = entry['pack_method'] & 0x1E0
|
pack_method = entry['pack_method'] & 0x1E0
|
||||||
|
|||||||
0
src/libs/nres/.gitkeep
Normal file
0
src/libs/nres/.gitkeep
Normal file
0
src/tools/nrescli/.gitkeep
Normal file
0
src/tools/nrescli/.gitkeep
Normal file
Reference in New Issue
Block a user