- Обновлены спецификации `runtime-pipeline`, `sound`, `terrain-map-loading`, `texture`, `ui` и `wear`. - Добавлены разделы о статусе покрытия и оставшихся задачах для достижения 100% завершенности. - Внесены уточнения по архитектурным ролям, минимальным контрактам и требованиям к toolchain для каждой подсистемы. - Уточнены форматы данных и правила взаимодействия между компонентами системы.
8.1 KiB
RsLi
RsLi — библиотечный контейнер ресурсов движка Parkan: Iron Strategy с зашифрованной таблицей записей и несколькими методами упаковки данных.
Страница описывает формат и runtime-контракт в высокоуровневом виде, без ссылок на внутренние адреса/функции дизассемблера.
Связанная страница:
1. Общая структура файла
[Header: 32]
[Entry table: entry_count * 32, XOR-encrypted]
[Packed payloads]
[Optional trailer: "AO" + overlay:u32]
В отличие от NRes, таблица записей у RsLi расположена в начале файла.
2. Заголовок (32 байта)
Все значения little-endian.
| Offset | Size | Type | Поле |
|---|---|---|---|
| 0 | 2 | char[2] | NL (магия) |
| 2 | 1 | u8 | зарезервировано, в retail = 0 |
| 3 | 1 | u8 | версия, в retail = 1 |
| 4 | 2 | i16 | entry_count (должен быть >= 0) |
| 14 | 2 | u16 | presorted_flag (0xABBA = таблица сортировки уже задана) |
| 20 | 4 | u32 | xor_seed |
Остальные байты заголовка считаются служебными и должны сохраняться без нормализации.
3. Таблица записей (после дешифровки)
Таблица начинается с offset = 32, размер entry_count * 32.
Каждая запись (32 байта):
| Offset | Size | Type | Поле |
|---|---|---|---|
| 0 | 12 | char[12] | name_raw (обычно uppercase ASCII, NUL optional) |
| 12 | 4 | bytes | служебный хвост, сохранять как есть |
| 16 | 2 | i16 | flags |
| 18 | 2 | i16 | sort_to_original |
| 20 | 4 | u32 | unpacked_size |
| 24 | 4 | u32 | data_offset_raw |
| 28 | 4 | u32 | packed_size |
3.1. Метод упаковки
method = flags & 0x1E0
Поддерживаемые значения:
| Маска | Метод |
|---|---|
0x000 |
без сжатия |
0x020 |
XOR only |
0x040 |
LZSS |
0x060 |
XOR + LZSS |
0x080 |
LZSS + адаптивный Huffman |
0x0A0 |
XOR + LZSS + адаптивный Huffman |
0x100 |
raw Deflate (RFC1951) |
Другие значения считаются неподдерживаемыми.
4. XOR-дешифрование таблицы и данных
Для таблицы и XOR-методов payload используется один и тот же потоковый XOR-алгоритм.
Ключ:
key16 = xor_seed & 0xFFFF(используются только младшие 16 бит seed).
Состояние:
lo = key16 & 0xFF
hi = key16 >> 8
Для каждого байта:
lo = hi XOR ((lo << 1) mod 256)
out = in XOR lo
hi = lo XOR (hi >> 1)
5. sort_to_original и поиск по имени
5.1. Режим presorted_flag == 0xABBA
sort_to_original обязан быть перестановкой 0..entry_count-1 без дубликатов.
5.2. Режим без presorted-флага
Слой загрузки строит sort_to_original самостоятельно:
- сортирует индексы по
strcmp-порядку имен (байтовое сравнение); - записывает эту перестановку в lookup-таблицу.
5.3. Поиск
Поиск выполняется бинарным поиском по lookup-таблице:
- запрос переводится в uppercase ASCII;
- на шаге бинарного поиска используется индекс
sort_to_original[mid]; - сравнение имен — bytewise (
strcmp-логика).
Fail-safe:
- при невалидном индексе lookup-таблицы выполняется линейный fallback.
6. AO-трейлер и media overlay
Опциональный трейлер в конце файла:
"AO" + overlay:u32
Если трейлер присутствует:
- эффективный offset payload:
effective_offset = data_offset_raw + overlay.
Ограничение:
overlay <= file_size.
7. Декодирование payload по методам
7.1. Без сжатия (0x000)
Берутся первые unpacked_size байт из packed-диапазона.
7.2. XOR only (0x020)
XOR-дешифрование первых unpacked_size байт.
7.3. LZSS (0x040, 0x060)
Параметры:
- ring buffer:
4096байт; - начальное заполнение ring:
0x20; - стартовый указатель ring:
0xFEE; - control-биты читаются LSB-first.
Правила:
bit=1: literal byte;bit=0: ссылка из 2 байт
offset = low | ((high & 0xF0) << 4)
length = (high & 0x0F) + 3.
Для 0x060 XOR применяется на лету к packed-потоку до LZSS-декодирования.
7.4. LZSS + адаптивный Huffman (0x080, 0x0A0)
Параметры:
N=4096,F=60,THRESHOLD=2;- адаптивное дерево Huffman обновляется по мере декодирования.
Для 0x0A0 XOR применяется на лету к битовому потоку до Huffman/LZSS-декодирования.
7.5. Deflate (0x100)
Используется raw Deflate-поток (RFC1951).
Важно:
- zlib-обертка (
RFC1950) не принимается.
8. Quirk: Deflate EOF+1
На retail-корпусе встречается один подтвержденный случай, где:
effective_offset + packed_size == file_size + 1.
Совместимое поведение:
- для метода
0x100допустить чтениеpacked_size - 1байт (если включен режим совместимости); - в строгом режиме считать это ошибкой.
9. Контрольные инварианты
Минимальные проверки:
magic == "NL",reserved == 0,version == 1.entry_count >= 0.table_end <= file_size.- Если
presorted_flag == 0xABBA,sort_to_original— валидная перестановка. effective_offset + packed_sizeне выходит за EOF (кроме разрешенного deflate EOF+1 quirk).- Итоговый распакованный размер равен
unpacked_size.
10. Эмпирическая проверка на retail-корпусе
Проверка на полном наборе testdata/Parkan - Iron Strategy:
- обнаружено
2архиваRsLi; - roundtrip
unpack -> repack -> byte-compare:2/2совпали побайтно; - подтвержден ровно один
deflate EOF+1случай (sprites.lib, entry23).
Инструменты:
tools/archive_roundtrip_validator.pycrates/rslitests
11. Статус покрытия и что осталось до 100%
Закрыто:
- формат заголовка/таблицы;
- XOR-алгоритм;
- все используемые методы декодирования;
- AO overlay;
- lookup-поиск и fallback;
- retail-валидация и побайтовый roundtrip.
Осталось до полного 100% архитектурного покрытия движка:
- Полная функциональная семантика битов
flagsвне маски метода (0x1E0) для геймплейных подсистем. - Канонический writer для авторинга новых архивов со стабильной стратегией выбора методов (
0x080/0x0A0/0x100) и параметров компрессии. - Формализация поведения для не-ASCII имен (на практике архивы используют ASCII-диапазон).