1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
|
# Документация по формату NRes
## Обзор
NRes — это формат контейнера ресурсов, используемый в игровом движке Nikita. Файл представляет собой архив, содержащий несколько упакованных файлов с метаданными и поддержкой различных методов сжатия.
## Структура файла NRes
### 1. Заголовок файла (16 байт)
```c
struct NResHeader {
uint32_t signature; // +0x00: Сигнатура "NRes" (0x7365526E в little-endian)
uint32_t version; // +0x04: Версия формата (0x00000100 = версия 1.0)
uint32_t fileCount; // +0x08: Количество файлов в архиве
uint32_t fileSize; // +0x0C: Общий размер файла в байтах
};
```
**Детали:**
- `signature`: Константа `0x7365526E` (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: Флаги метода упаковки и опции
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 (основной метод)
#define PACK_METHOD_MASK2 0x1C0 // Биты 6-7 (альтернативная маска)
// Методы упаковки (биты 5-8)
#define PACK_NONE 0x000 // Нет упаковки (копирование)
#define PACK_XOR 0x020 // XOR-шифрование
#define PACK_FRES 0x040 // FRES компрессия (устаревшая)
#define PACK_FRES_XOR 0x060 // FRES + XOR (два прохода)
#define PACK_ZLIB 0x080 // Zlib сжатие (устаревшее)
#define PACK_ZLIB_XOR 0x0A0 // Zlib + XOR (два прохода)
#define PACK_HUFFMAN 0x0E0 // Huffman кодирование (основной метод)
// Дополнительные флаги
#define FLAG_ENCRYPTED 0x040 // Файл зашифрован/требует декодирования
```
**Алгоритм определения метода:**
1. Извлечь биты `packMethod & 0x1E0`
2. Проверить конкретные значения:
- `0x000`: Данные не сжаты, простое копирование
- `0x020`: XOR-шифрование с двухбайтовым ключом
- `0x040` или `0x060`: FRES компрессия (может быть + XOR)
- `0x080` или `0x0A0`: Zlib компрессия (может быть + XOR)
- `0x0E0`: Huffman кодирование (наиболее распространенный)
### Поле: 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 байта)
- **Назначение**: Индекс для быстрого поиска по отсортированному каталогу
- **Использование**:
- Каталог сортируется по алфавиту (имени файлов)
- `sortIndex` хранит оригинальный порядковый номер файла
- Позволяет использовать бинарный поиск для функции `rsFind()`
### Поле: reserved (смещение +0x30, 16 байт)
- **Назначение**: Зарезервировано для будущих расширений
- **В файле**: Обычно заполнено нулями
- **Может содержать**: Дополнительные метаданные в новых версиях формата
## Алгоритмы упаковки
### 1. Без упаковки (PACK_NONE = 0x000)
```
Простое копирование данных:
memcpy(destination, source, packedSize);
```
### 2. XOR-шифрование (PACK_XOR = 0x020)
```c
// Ключ берется из поля crc32
uint16_t key = (uint16_t)(crc32 & 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-битный ключ из младших байт CRC32
- Ключ изменяется после каждого байта по специальному алгоритму
- Операции: XOR с старшим байтом ключа и со сдвинутым значением
### 3. [FRES компрессия](fres_decompression.md) (PACK_FRES = 0x040, 0x060)
Алгоритм FRES — это RLE-подобное сжатие с особой кодировкой повторов:
```
sub_1001B22E() - функция декомпрессии FRES
- Читает управляющие байты
- Декодирует литералы и повторы
- Использует скользящее окно для ссылок
```
### 4. [Huffman кодирование](huffman_decompression.md) (PACK_HUFFMAN = 0x0E0)
Наиболее сложный и эффективный метод:
```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. Инициализация структуры декодера
2. Чтение битов и построение дерева Huffman
3. Декодирование символов по дереву
4. Запись в выходной буфер
## Высокоуровневая инструкция по реализации
### Этап 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 != 0x7365526E: # "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()
# Используем бинарный поиск, так как каталог отсортирован
# Сортировка по 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['crc32'], 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['crc32'], entry['xor_size'])
return unpack_fres(temp_data, entry['unpacked_size'])
else:
return unpack_fres(packed_data, entry['unpacked_size'])
elif pack_method == 0x0E0:
# Huffman кодирование
return unpack_huffman(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, crc32, size):
"""XOR-дешифрование с изменяющимся ключом"""
result = bytearray(size)
key = crc32 & 0xFFFF # Берем младшие 16 бит
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_huffman(data, unpacked_size):
"""
Huffman декодирование (DEFLATE-подобный)
Полная реализация в nres_decompression.py (класс HuffmanDecoder)
"""
from nres_decompression import HuffmanDecoder
decoder = HuffmanDecoder()
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. Порядок байт (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 == 0x7365526E
except:
return False
```
### Получение информации о файле
```python
def get_file_info(entry):
pack_names = {
0x000: "Без сжатия",
0x020: "XOR",
0x040: "FRES",
0x060: "FRES+XOR",
0x080: "Zlib",
0x0A0: "Zlib+XOR",
0x0E0: "Huffman"
}
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 представляет собой эффективный архив с поддержкой множества методов сжатия.
|