Files
fparkan/docs/specs/rsli.md
Valentin Popov 0d7ae6a017
Some checks failed
Test / Lint (push) Failing after 1m10s
Test / Test (push) Has been skipped
Test / Render parity (push) Has been skipped
Документирование и обновление спецификаций
- Обновлены спецификации `runtime-pipeline`, `sound`, `terrain-map-loading`, `texture`, `ui` и `wear`.
- Добавлены разделы о статусе покрытия и оставшихся задачах для достижения 100% завершенности.
- Внесены уточнения по архитектурным ролям, минимальным контрактам и требованиям к toolchain для каждой подсистемы.
- Уточнены форматы данных и правила взаимодействия между компонентами системы.
2026-02-19 11:07:04 +04:00

8.1 KiB
Raw Permalink Blame History

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-таблице:

  1. запрос переводится в uppercase ASCII;
  2. на шаге бинарного поиска используется индекс sort_to_original[mid];
  3. сравнение имен — 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. Контрольные инварианты

Минимальные проверки:

  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-диапазон).