aboutsummaryrefslogtreecommitdiff
path: root/docs/specs/rsli.md
diff options
context:
space:
mode:
Diffstat (limited to 'docs/specs/rsli.md')
-rw-r--r--docs/specs/rsli.md230
1 files changed, 230 insertions, 0 deletions
diff --git a/docs/specs/rsli.md b/docs/specs/rsli.md
new file mode 100644
index 0000000..298cf2a
--- /dev/null
+++ b/docs/specs/rsli.md
@@ -0,0 +1,230 @@
+# RsLi
+
+`RsLi` — библиотечный контейнер ресурсов движка Parkan: Iron Strategy с зашифрованной таблицей записей и несколькими методами упаковки данных.
+
+Страница описывает формат и runtime-контракт в высокоуровневом виде, без ссылок на внутренние адреса/функции дизассемблера.
+
+Связанная страница:
+
+- [NRes](nres.md)
+
+## 1. Общая структура файла
+
+```text
+[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).
+
+Состояние:
+
+```text
+lo = key16 & 0xFF
+hi = key16 >> 8
+```
+
+Для каждого байта:
+
+```text
+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-таблице:
+
+1. запрос переводится в uppercase ASCII;
+2. на шаге бинарного поиска используется индекс `sort_to_original[mid]`;
+3. сравнение имен — bytewise (`strcmp`-логика).
+
+Fail-safe:
+
+- при невалидном индексе lookup-таблицы выполняется линейный fallback.
+
+## 6. AO-трейлер и media overlay
+
+Опциональный трейлер в конце файла:
+
+```text
+"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. Контрольные инварианты
+
+Минимальные проверки:
+
+1. `magic == "NL"`, `reserved == 0`, `version == 1`.
+2. `entry_count >= 0`.
+3. `table_end <= file_size`.
+4. Если `presorted_flag == 0xABBA`, `sort_to_original` — валидная перестановка.
+5. `effective_offset + packed_size` не выходит за EOF (кроме разрешенного deflate EOF+1 quirk).
+6. Итоговый распакованный размер равен `unpacked_size`.
+
+## 10. Эмпирическая проверка на retail-корпусе
+
+Проверка на полном наборе `testdata/Parkan - Iron Strategy`:
+
+- обнаружено `2` архива `RsLi`;
+- roundtrip `unpack -> repack -> byte-compare`: `2/2` совпали побайтно;
+- подтвержден ровно один `deflate EOF+1` случай (`sprites.lib`, entry `23`).
+
+Инструменты:
+
+- `tools/archive_roundtrip_validator.py`
+- `crates/rsli` tests
+
+## 11. Статус покрытия и что осталось до 100%
+
+Закрыто:
+
+- формат заголовка/таблицы;
+- XOR-алгоритм;
+- все используемые методы декодирования;
+- AO overlay;
+- lookup-поиск и fallback;
+- retail-валидация и побайтовый roundtrip.
+
+Осталось до полного 100% архитектурного покрытия движка:
+
+1. Полная функциональная семантика битов `flags` вне маски метода (`0x1E0`) для геймплейных подсистем.
+2. Канонический writer для авторинга новых архивов со стабильной стратегией выбора методов (`0x080/0x0A0/0x100`) и параметров компрессии.
+3. Формализация поведения для не-ASCII имен (на практике архивы используют ASCII-диапазон).