From 0d7ae6a017b8b2bf26c5c14c39cb62b599e8262d Mon Sep 17 00:00:00 2001 From: Valentin Popov Date: Thu, 19 Feb 2026 11:07:04 +0400 Subject: Документирование и обновление спецификаций MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Обновлены спецификации `runtime-pipeline`, `sound`, `terrain-map-loading`, `texture`, `ui` и `wear`. - Добавлены разделы о статусе покрытия и оставшихся задачах для достижения 100% завершенности. - Внесены уточнения по архитектурным ролям, минимальным контрактам и требованиям к toolchain для каждой подсистемы. - Уточнены форматы данных и правила взаимодействия между компонентами системы. --- docs/specs/rsli.md | 230 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 230 insertions(+) create mode 100644 docs/specs/rsli.md (limited to 'docs/specs/rsli.md') 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-диапазон). -- cgit v1.2.3