562 lines
36 KiB
Markdown
562 lines
36 KiB
Markdown
|
|
# III. Ресурсная система и форматы
|
|||
|
|
|
|||
|
|
Ресурсная система Iron3D переводит имена из миссий и прототипов в объекты,
|
|||
|
|
которыми пользуются подсистемы мира, рендера, анимации, звука, эффектов и
|
|||
|
|
управления. В этом пути участвуют несколько разных сущностей: файл на диске,
|
|||
|
|
открытый архив, запись каталога, подготовленный payload и готовый runtime-объект.
|
|||
|
|
Их нельзя смешивать, потому что у каждого уровня свой срок жизни, свои правила
|
|||
|
|
кэширования и свой набор проверок.
|
|||
|
|
|
|||
|
|
Основной контейнер ресурсов -- [NRes](../reference/nres.md). Он используется как
|
|||
|
|
внешний архив (`objects.rlb`, `Material.lib`, `Textures.lib`) и как внутренний
|
|||
|
|
контейнер модели `*.msh`. Второй библиотечный формат -- [RsLi](../reference/rsli.md):
|
|||
|
|
его каталог находится в начале файла, а payload может храниться raw, через
|
|||
|
|
потоковое преобразование, LZSS, адаптивный Huffman + LZSS или raw Deflate.
|
|||
|
|
Визуальная часть прототипа дальше проходит через [MSH](../reference/msh.md),
|
|||
|
|
[WEAR/MAT0](../reference/materials.md) и [Texm](../reference/texm.md), но этот
|
|||
|
|
том описывает именно ресурсный слой: как найти, проверить, раскрыть и сохранить
|
|||
|
|
данные до передачи их предметным подсистемам.
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
TMA или unit DAT
|
|||
|
|
-> логический ключ
|
|||
|
|
-> objects.rlb
|
|||
|
|
-> archive.rlb :: model.msh
|
|||
|
|
-> model.wea
|
|||
|
|
-> Material.lib :: MAT0
|
|||
|
|
-> Textures.lib / LightMap.lib :: Texm
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
На демо-корпусе эта цепочка проверена целиком для всех реально размещённых
|
|||
|
|
объектов. При этом полная таблица прототипов может содержать ссылки на контент,
|
|||
|
|
которого нет в урезанной поставке. Диагностика должна различать недостижимую
|
|||
|
|
ссылку в общем реестре и ресурс, реально требуемый выбранной миссией.
|
|||
|
|
|
|||
|
|
## Ресурсный конвейер
|
|||
|
|
|
|||
|
|
Загрузка ресурса состоит из последовательных стадий:
|
|||
|
|
|
|||
|
|
1. Разрешить относительный путь с учётом глобального resource path и текущего
|
|||
|
|
каталога игры.
|
|||
|
|
2. Открыть архив или вернуть уже открытый archive object из кэша.
|
|||
|
|
3. Найти запись каталога по имени, не меняя исходный порядок каталога.
|
|||
|
|
4. Проверить bounds, размер payload и способ хранения.
|
|||
|
|
5. Подготовить bytes: распаковать, применить потоковое преобразование или
|
|||
|
|
вернуть raw-диапазон.
|
|||
|
|
6. Разобрать предметный формат и создать объект подсистемы.
|
|||
|
|
7. Сохранить готовый объект в отдельном кэше, если формат допускает повторное
|
|||
|
|
использование.
|
|||
|
|
|
|||
|
|
Эти стадии дают четыре независимых уровня кэша:
|
|||
|
|
|
|||
|
|
1. Открытые архивы.
|
|||
|
|
2. Каталоги имён, offsets и размеров.
|
|||
|
|
3. Подготовленные блоки данных.
|
|||
|
|
4. Кэши моделей, материалов, текстур, lightmaps, эффектов и служебных объектов.
|
|||
|
|
|
|||
|
|
Повторное открытие того же нормализованного пути возвращает существующий
|
|||
|
|
archive object и увеличивает счётчик владельцев. Готовая texture или model при
|
|||
|
|
этом может жить дольше file handle и иметь собственную политику удаления. Кэш
|
|||
|
|
предметного объекта не должен напрямую закрывать архив: он зависит от данных,
|
|||
|
|
но не владеет файлом как ресурсом операционной системы.
|
|||
|
|
|
|||
|
|
## Имена и пути
|
|||
|
|
|
|||
|
|
Большинство игровых имён сравнивается без учёта регистра в ASCII-диапазоне. Это
|
|||
|
|
не Unicode case folding. Для совместимости достаточно нормализовать `A..Z` в
|
|||
|
|
`a..z`, а для RsLi-поиска -- переводить запрос в uppercase ASCII и укладывать его
|
|||
|
|
в фиксированный ключ.
|
|||
|
|
|
|||
|
|
Фиксированные строки читаются bounded parser-ом: строковая часть заканчивается
|
|||
|
|
на первом NUL, но оставшийся хвост поля сохраняется. Нельзя очищать хвосты,
|
|||
|
|
пересобирать регистр, заменять смешанные разделители или заранее переводить все
|
|||
|
|
пути в абсолютные имена. Старые данные используют исторические имена библиотек,
|
|||
|
|
разный регистр исходных путей и фиксированные поля, где после терминатора могут
|
|||
|
|
оставаться значимые для roundtrip bytes.
|
|||
|
|
|
|||
|
|
## Строгий и совместимый режимы
|
|||
|
|
|
|||
|
|
Строгий reader нужен тестам, редактору и проверке корпуса. Он валидирует
|
|||
|
|
структуру до выдачи любого `EntryView`: magic, версию, счётчики, арифметические
|
|||
|
|
переполнения, bounds, sort permutation, alignment и точное завершение payload.
|
|||
|
|
Если формат требует NUL-терминатор, строгий режим проверяет его именно в пределах
|
|||
|
|
фиксированного поля.
|
|||
|
|
|
|||
|
|
Совместимый reader повторяет только известные особенности оригинала:
|
|||
|
|
|
|||
|
|
- линейный поиск при повреждённой сортировочной таблице;
|
|||
|
|
- RsLi-исключение `deflate_eof_plus_one` для `sprites.lib::INTERF8.TEX`;
|
|||
|
|
- material fallbacks, подтверждённые ресурсной цепочкой;
|
|||
|
|
- отсутствие геометрии у системных и солнечных объектов, где mesh pass не
|
|||
|
|
требуется.
|
|||
|
|
|
|||
|
|
Режим совместимости не должен скрывать произвольные ошибки. Каждое послабление
|
|||
|
|
оформляется как именованное правило и покрывается отдельным тестом. Если quirk
|
|||
|
|
применим только к Deflate-записи, он не распространяется на LZSS, Huffman или
|
|||
|
|
raw-диапазоны.
|
|||
|
|
|
|||
|
|
## NRes
|
|||
|
|
|
|||
|
|
`NRes` хранит произвольные именованные payload и их атрибуты. Каталог расположен
|
|||
|
|
в конце файла, поэтому начало каталога вычисляется из полного размера файла и
|
|||
|
|
числа записей.
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
[Header: 16 байт]
|
|||
|
|
[Data region: payload с выравниванием]
|
|||
|
|
[Directory: entry_count x 64 байта]
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Все числа little-endian.
|
|||
|
|
|
|||
|
|
```c
|
|||
|
|
struct NResHeader16 {
|
|||
|
|
char magic[4]; // "NRes"
|
|||
|
|
uint32_t version; // 0x00000100
|
|||
|
|
int32_t entry_count; // >= 0
|
|||
|
|
uint32_t total_size; // равен фактическому размеру файла
|
|||
|
|
};
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Производные значения:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
directory_size = entry_count * 64
|
|||
|
|
directory_offset = total_size - directory_size
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Reader проверяет, что `directory_offset >= 16`, умножение не переполнено, а
|
|||
|
|
каталог заканчивается точно на `total_size`.
|
|||
|
|
|
|||
|
|
### Запись каталога NRes
|
|||
|
|
|
|||
|
|
```c
|
|||
|
|
#pragma pack(push, 1)
|
|||
|
|
struct NResEntry64 {
|
|||
|
|
uint32_t type_id; // +0x00
|
|||
|
|
uint32_t attr1; // +0x04
|
|||
|
|
uint32_t attr2; // +0x08
|
|||
|
|
uint32_t size; // +0x0C
|
|||
|
|
uint32_t attr3; // +0x10
|
|||
|
|
char name[36]; // +0x14
|
|||
|
|
uint32_t data_offset; // +0x38
|
|||
|
|
uint32_t sort_index; // +0x3C
|
|||
|
|
};
|
|||
|
|
#pragma pack(pop)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Имя содержит не более 35 полезных байт и завершающий ноль. Writer запрещает
|
|||
|
|
внутренний NUL и слишком длинное имя, но сохраняет неизвестные атрибуты
|
|||
|
|
`attr1`, `attr2`, `attr3` без нормализации. Их смысл зависит от конкретного
|
|||
|
|
типа ресурса и не может быть выведен из контейнера.
|
|||
|
|
|
|||
|
|
Поле `sort_index` задаёт отображение из позиции в отсортированном списке в
|
|||
|
|
исходный индекс записи. Каталог остаётся в исходном порядке. Поиск идёт по
|
|||
|
|
отсортированному отображению, но возвращает исходную запись. При сохранении
|
|||
|
|
writer строит массив исходных индексов, сортирует его по ASCII-case-insensitive
|
|||
|
|
именам и записывает результат в `sort_index`. Если отображение нельзя использовать
|
|||
|
|
или оно не является перестановкой в строгом режиме, совместимый путь переходит к
|
|||
|
|
последовательному сравнению имён.
|
|||
|
|
|
|||
|
|
### Размещение данных NRes
|
|||
|
|
|
|||
|
|
Каждый active payload должен лежать после 16-байтового заголовка и полностью до
|
|||
|
|
начала каталога. Канонические игровые файлы выравнивают начало следующего
|
|||
|
|
payload до границы 8 байт нулевым заполнением.
|
|||
|
|
|
|||
|
|
Порядок canonical save:
|
|||
|
|
|
|||
|
|
1. Записать временный заголовок.
|
|||
|
|
2. Записать payload всех записей в текущем порядке.
|
|||
|
|
3. После каждого блока добавить нули до кратности 8.
|
|||
|
|
4. Построить таблицу поиска имён.
|
|||
|
|
5. Дописать каталог.
|
|||
|
|
6. Записать окончательный `total_size`.
|
|||
|
|
|
|||
|
|
Строгий reader выполняет проверки до выдачи записи:
|
|||
|
|
|
|||
|
|
- `magic == "NRes"` и `version == 0x100`;
|
|||
|
|
- `entry_count >= 0`, а `entry_count * 64` вычисляется без переполнения;
|
|||
|
|
- `total_size` равен фактической длине файла;
|
|||
|
|
- `directory_offset = total_size - entry_count * 64` не меньше 16;
|
|||
|
|
- для каждой записи `data_offset >= 16` и `data_offset + size <= directory_offset`;
|
|||
|
|
- поле имени содержит NUL в пределах 36 байт;
|
|||
|
|
- каждый `sort_index < entry_count`;
|
|||
|
|
- в строгом режиме все `sort_index` образуют перестановку `0..N-1`.
|
|||
|
|
|
|||
|
|
Нулевое заполнение до границы 8 байт -- подтверждённое поведение игровых
|
|||
|
|
архивов и canonical writer-а. Reader не должен считать ненулевой gap частью
|
|||
|
|
соседнего payload, но lossless-редактор сохраняет исходные bytes, если файл
|
|||
|
|
открыт не в режиме канонической пересборки.
|
|||
|
|
|
|||
|
|
### Неплотная data region
|
|||
|
|
|
|||
|
|
Проверка 120 NRes-файлов / 6 804 entries Части 1 и 134 файлов / 8 171 entries
|
|||
|
|
Части 2 не выявила нарушений magic, version, total size, bounds, sort
|
|||
|
|
permutation, ASCII-order, 8-byte alignment или перекрытий активных payload.
|
|||
|
|
Однако `Textures.lib` Части 2 содержит большой ненулевой диапазон в data region,
|
|||
|
|
который не адресуется ни одной записью каталога. Первый активный payload
|
|||
|
|
начинается значительно позже начала файла, а каталог и все активные entries
|
|||
|
|
остаются корректными.
|
|||
|
|
|
|||
|
|
Следовательно, parser не должен требовать плотного покрытия data region. Нужно
|
|||
|
|
различать три вида диапазонов:
|
|||
|
|
|
|||
|
|
- `active payload` -- bytes, на которые указывает запись каталога;
|
|||
|
|
- `gap/padding` -- bytes между активными диапазонами;
|
|||
|
|
- `unindexed preserved region` -- произвольные bytes, не принадлежащие ни одной
|
|||
|
|
записи.
|
|||
|
|
|
|||
|
|
Canonical compact writer может исключить unindexed region только при явной
|
|||
|
|
операции repack. Lossless editor сохраняет её побайтно вместе с исходным
|
|||
|
|
порядком entries и gaps.
|
|||
|
|
|
|||
|
|
## RsLi
|
|||
|
|
|
|||
|
|
`RsLi` -- библиотечный архив с каталогом в начале файла. Записи могут храниться
|
|||
|
|
в исходном виде или проходить один из поддержанных путей подготовки.
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
[Header: 32 байта]
|
|||
|
|
[Entry table: entry_count x 32 байта]
|
|||
|
|
[Payloads]
|
|||
|
|
[необязательный trailer]
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Заголовок начинается с двух байт `NL`. Версия равна `1`, число записей хранится
|
|||
|
|
как знаковое 16-битное значение. Поле по смещению `0x0E` может содержать
|
|||
|
|
`0xABBA`: это означает, что отображение сортировки уже подготовлено.
|
|||
|
|
|
|||
|
|
Подтверждённые поля header:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
+0x00 char[2] "NL"
|
|||
|
|
+0x02 u8 reserved, в корпусе 0
|
|||
|
|
+0x03 u8 version, в корпусе 1
|
|||
|
|
+0x04 i16 entry_count
|
|||
|
|
+0x0E u16 presorted_flag, значение 0xABBA
|
|||
|
|
+0x14 u32 xor_seed
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Остальные bytes заголовка сохраняются без нормализации.
|
|||
|
|
|
|||
|
|
### Запись каталога RsLi
|
|||
|
|
|
|||
|
|
После подготовки таблицы каждая запись имеет layout 32 байта:
|
|||
|
|
|
|||
|
|
```c
|
|||
|
|
struct RsLiEntry32 {
|
|||
|
|
char name[12];
|
|||
|
|
uint8_t service[4];
|
|||
|
|
int16_t flags;
|
|||
|
|
int16_t sort_to_original;
|
|||
|
|
uint32_t unpacked_size;
|
|||
|
|
uint32_t data_offset_raw;
|
|||
|
|
uint32_t packed_size;
|
|||
|
|
};
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Имя обычно хранится в uppercase ASCII. Четыре служебных байта после имени
|
|||
|
|
сохраняются без изменения. `sort_to_original` играет ту же роль, что и
|
|||
|
|
`sort_index` в NRes: связывает отсортированную позицию с исходной записью.
|
|||
|
|
|
|||
|
|
Таблица на диске проходит обратимое побайтовое преобразование. Начальное
|
|||
|
|
состояние берётся из младших 16 бит `xor_seed`. Если обозначить два байта
|
|||
|
|
состояния как `lo` и `hi`, для каждого входного байта выполняется:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
lo = hi XOR ((lo << 1) mod 256)
|
|||
|
|
out = in XOR lo
|
|||
|
|
hi = lo XOR (hi >> 1)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Операция симметрична: один и тот же цикл используется для подготовки и
|
|||
|
|
восстановления. Состояние непрерывно проходит по всей таблице; его нельзя
|
|||
|
|
перезапускать на каждой записи.
|
|||
|
|
|
|||
|
|
### Способы хранения RsLi
|
|||
|
|
|
|||
|
|
Способ определяется выражением `flags & 0x1E0`:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
0x000 исходный блок
|
|||
|
|
0x020 только потоковое байтовое преобразование
|
|||
|
|
0x040 LZSS
|
|||
|
|
0x060 преобразование, затем LZSS
|
|||
|
|
0x080 адаптивный Huffman, затем LZSS
|
|||
|
|
0x0A0 преобразование, адаптивный Huffman и LZSS
|
|||
|
|
0x100 raw Deflate без оболочки zlib
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Reader обязан различать все значения, а неизвестную маску отклонять как
|
|||
|
|
неподдерживаемую. После любого пути должно быть получено ровно `unpacked_size`
|
|||
|
|
байт. Методы `0x080` и `0x0A0` подтверждены decoder-кодом и синтетическими
|
|||
|
|
тестами, но живых payload этих веток в проверенных RsLi-файлах не найдено.
|
|||
|
|
|
|||
|
|
Параметры LZSS:
|
|||
|
|
|
|||
|
|
- размер кольцевого окна -- `4096`;
|
|||
|
|
- начальное заполнение -- байт `0x20`;
|
|||
|
|
- начальная позиция -- `0xFEE`;
|
|||
|
|
- управляющие признаки читаются от младшего бита к старшему;
|
|||
|
|
- двухбайтовая ссылка кодирует 12-битную позицию и длину `n + 3`;
|
|||
|
|
- восстановленные bytes сразу записываются обратно в кольцевое окно.
|
|||
|
|
|
|||
|
|
В конце файла может находиться шестибайтовый media overlay trailer: два символа
|
|||
|
|
`AO` и 32-битное значение `overlay`. В таком режиме фактическая позиция блока
|
|||
|
|
равна `data_offset_raw + overlay`. Reader сначала проверяет, что overlay не
|
|||
|
|
выходит за размер отображённого файла, затем проверяет весь диапазон записи.
|
|||
|
|
|
|||
|
|
### Поиск, кэш и проверки RsLi
|
|||
|
|
|
|||
|
|
Запрос имени переводится в uppercase ASCII и укладывается в фиксированный ключ.
|
|||
|
|
При признаке `0xABBA` используется сохранённое отображение сортировки. Если
|
|||
|
|
признака нет, loader строит его после чтения каталога. Некорректный индекс
|
|||
|
|
приводит к последовательному поиску.
|
|||
|
|
|
|||
|
|
Файл открывается через memory mapping. Runtime-запись хранит указатель на
|
|||
|
|
упакованный диапазон, размеры и необязательный указатель на подготовленные
|
|||
|
|
данные. Первый обычный `load` создаёт буфер и сохраняет результат; повторный
|
|||
|
|
возвращает его из кэша. Быстрый путь может вернуть указатель непосредственно в
|
|||
|
|
mapped file только для исходного блока.
|
|||
|
|
|
|||
|
|
Reader проверяет:
|
|||
|
|
|
|||
|
|
- сигнатуру `NL`, служебный байт и версию;
|
|||
|
|
- неотрицательное число записей;
|
|||
|
|
- размещение всей таблицы в файле;
|
|||
|
|
- что сохранённое отображение сортировки является перестановкой;
|
|||
|
|
- что эффективный диапазон каждого блока не выходит за конец файла;
|
|||
|
|
- что способ хранения известен;
|
|||
|
|
- что после подготовки получено ровно `unpacked_size` байт.
|
|||
|
|
|
|||
|
|
В demo-каталоге и полных каталогах обеих частей наблюдаются два RsLi-файла:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
gamefont.rlb 2 entries, все 0x040 LZSS
|
|||
|
|
sprites.lib 24 entries, все 0x100 raw Deflate
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Последняя запись `sprites.lib::INTERF8.TEX` объявляет packed range, который
|
|||
|
|
заканчивается на один байт после физического EOF. Совместимый путь читает на
|
|||
|
|
один байт меньше; строгий путь регистрирует именованный quirk
|
|||
|
|
`deflate_eof_plus_one`. Это исключение не распространяется на другие записи,
|
|||
|
|
методы или произвольные выходы за конец файла.
|
|||
|
|
|
|||
|
|
Writer, который редактирует существующий архив, сохраняет все служебные bytes
|
|||
|
|
заголовка и записей. Выбор оптимального способа упаковки для новых файлов
|
|||
|
|
является отдельной политикой и не должен менять уже существующие entries без
|
|||
|
|
явного запроса.
|
|||
|
|
|
|||
|
|
## Реестр объектов
|
|||
|
|
|
|||
|
|
Имя объекта в миссии является логическим ключом. Связь этого ключа с файлами
|
|||
|
|
модели, материалов и служебных данных хранится в `objects.rlb`, который сам
|
|||
|
|
использует формат NRes. Имя записи каталога -- ключ прототипа. Payload записи
|
|||
|
|
состоит из записей по 64 байта:
|
|||
|
|
|
|||
|
|
```c
|
|||
|
|
struct ObjectRef64 {
|
|||
|
|
char archive_name[32];
|
|||
|
|
char resource_name[32];
|
|||
|
|
};
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Payload каждой записи `objects.rlb` обязан быть кратен 64 байтам. Это
|
|||
|
|
проверяется до чтения первой ссылки. Оба поля читаются как строки до первого
|
|||
|
|
NUL, но полный 32-байтовый блок сохраняется при редактировании без очистки
|
|||
|
|
хвоста.
|
|||
|
|
|
|||
|
|
Разрешение прототипа:
|
|||
|
|
|
|||
|
|
1. Найти entry реестра по логическому ключу без учёта ASCII-регистра.
|
|||
|
|
2. Прочитать все `ObjectRef64` в исходном порядке.
|
|||
|
|
3. Если ссылка указывает обратно в `objects.rlb`, рекурсивно раскрыть указанный
|
|||
|
|
родительский prototype.
|
|||
|
|
4. Объединить effective references родителя с локальными references дочерней
|
|||
|
|
записи, сохранив порядок и происхождение.
|
|||
|
|
5. Выбрать первую существующую ссылку с расширением `.msh`, открыть указанный
|
|||
|
|
архив и найти модель по имени.
|
|||
|
|
6. Загружать `.bas` как отдельный служебный ресурс сооружения, а не как замену
|
|||
|
|
MSH.
|
|||
|
|
7. Если effective prototype не содержит MSH, считать объект негеометрическим,
|
|||
|
|
если это допускает его назначение.
|
|||
|
|
|
|||
|
|
Resolver обязан детектировать циклы наследования, ограничивать глубину и
|
|||
|
|
кэшировать результат раскрытия. В обеих частях fortification-прототипы используют
|
|||
|
|
явного родителя из `objects.rlb`: родитель предоставляет MSH/WEAR/CPT/NDP/CTL,
|
|||
|
|
а дочерняя запись добавляет собственный BASE. Негеометрический объект не является
|
|||
|
|
ошибкой сам по себе: системные и солнечные сущности могут участвовать в логике
|
|||
|
|
или эффектах без mesh pass.
|
|||
|
|
|
|||
|
|
Контракт реализации:
|
|||
|
|
|
|||
|
|
- сохранять порядок ссылок внутри прототипа;
|
|||
|
|
- не выводить имя модели из имени entry, если имеется явная ссылка;
|
|||
|
|
- проверять существование указанного архива и ресурса независимо;
|
|||
|
|
- отделять статус «негеометрический объект» от статуса «повреждённая ссылка»;
|
|||
|
|
- кэшировать результат разрешения ключа, но инвалидировать его при замене архива;
|
|||
|
|
- в diagnostic mode строить полный граф зависимостей и отмечать узлы, достижимые
|
|||
|
|
из выбранной миссии.
|
|||
|
|
|
|||
|
|
В demo-варианте `objects.rlb` содержит 590 прототипов. У 554 есть прямая ссылка
|
|||
|
|
на MSH; 549 таких ссылок разрешаются в доступных demo-архивах. Ещё 34 прототипа
|
|||
|
|
раскрываются через родительскую запись `objects.rlb` и дополняются локальным
|
|||
|
|
BASE. Семь записей не дают геометрию, а 41 ссылка всего реестра указывает на
|
|||
|
|
контент, которого нет в урезанной поставке. Для 501 запросов прототипов,
|
|||
|
|
порождаемых шестью demo-миссиями, найдены прототип, MSH и WEAR.
|
|||
|
|
|
|||
|
|
## Unit DAT
|
|||
|
|
|
|||
|
|
Запись миссии может ссылаться не на один ключ, а на unit-файл `*.dat`. Такой файл
|
|||
|
|
перечисляет компоненты сложного игрового объекта.
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
TMA object
|
|||
|
|
-> путь к unit DAT
|
|||
|
|
-> список component keys
|
|||
|
|
-> несколько entries objects.rlb
|
|||
|
|
-> модели, WEAR, control points, effects и другие ресурсы
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Это объясняет, почему один размещённый unit может состоять из корпуса, башен,
|
|||
|
|
оружия, эффектов и служебных частей. В демоверсии найдено 425 unit-файлов и
|
|||
|
|
5 219 записей; все разобраны без ошибок. Наблюдаемый тип записи равен `1`, а
|
|||
|
|
архив назначения -- `objects.rlb`. В 5 205 из 5 219 фиксированных полей имени
|
|||
|
|
обнаружены ненулевые bytes после строкового терминатора; reader использует
|
|||
|
|
строковую часть, а lossless writer сохраняет весь исходный блок.
|
|||
|
|
|
|||
|
|
Размер каждого unit DAT удовлетворяет формуле:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
file_size = 8 + record_count * 112
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Первые два байта header равны `F1 F0`. Оставшиеся шесть bytes имеют несколько
|
|||
|
|
наблюдаемых вариантов; их семантика пока не названа и они сохраняются как
|
|||
|
|
`header_opaque[6]`.
|
|||
|
|
|
|||
|
|
```c
|
|||
|
|
#pragma pack(push, 1)
|
|||
|
|
struct UnitDatRecord112 {
|
|||
|
|
char archive_name[32]; // +0x00
|
|||
|
|
char resource_name[32]; // +0x20
|
|||
|
|
uint32_t kind; // +0x40, в корпусе всегда 1
|
|||
|
|
int32_t parent_or_link; // +0x44
|
|||
|
|
char description[32]; // +0x48
|
|||
|
|
uint32_t tail0; // +0x68, opaque
|
|||
|
|
uint32_t tail1; // +0x6C, opaque
|
|||
|
|
};
|
|||
|
|
#pragma pack(pop)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Во всех проверенных records `archive_name == "objects.rlb"` и `kind == 1`.
|
|||
|
|
Поле `parent_or_link` встречается как `-1`, `0`, `1` и другие небольшие индексы
|
|||
|
|
и связывает компоненты составного unit; точная предметная классификация ссылки
|
|||
|
|
ещё не закрыта. `description` -- человекочитаемое описание компонента. В Части 2
|
|||
|
|
есть поля `description[32]`, полностью заполненные без NUL; это валидная bounded
|
|||
|
|
string длиной 32 байта. Требование обязательного terminator применяется только
|
|||
|
|
к полям, где оно доказано форматом. `tail0` и `tail1` нельзя нормализовать.
|
|||
|
|
|
|||
|
|
Проверено 425 файлов / 5 219 records Части 1 и 676 файлов / 8 145 records
|
|||
|
|
Части 2. Все соответствуют формуле размера, `kind == 1` и
|
|||
|
|
`archive_name == "objects.rlb"`.
|
|||
|
|
|
|||
|
|
## Вспомогательные форматы
|
|||
|
|
|
|||
|
|
MSH, материал и текстура отвечают за видимую форму. Полноценный прототип
|
|||
|
|
дополнительно хранит точки крепления, зависимости, управляющие параметры,
|
|||
|
|
области взаимодействия и ссылки на эффекты. Эти данные распределены между
|
|||
|
|
несколькими небольшими форматами.
|
|||
|
|
|
|||
|
|
Для них действует строгая граница знания: framing, counts и валидность корпуса
|
|||
|
|
могут быть подтверждены parser-ом, тогда как предметный смысл части полей
|
|||
|
|
остаётся неизвестным. Reader предоставляет typed view для доказанных полей и
|
|||
|
|
raw bytes для остальных. Инструмент должен показывать статус поля:
|
|||
|
|
`layout-confirmed`, `consumer-inferred` или `opaque`.
|
|||
|
|
|
|||
|
|
### CTPT
|
|||
|
|
|
|||
|
|
В demo-корпусе найдено 284 CTPT-ресурса и 3 599 точек; все прочитаны без ошибок.
|
|||
|
|
Имена показывают назначение слоя: `TurretCenter`, `TurretDirect`,
|
|||
|
|
`CameraCenter`, `TargetDirect`, `Root`, `Sfx_1`, `Sign_Entrance1`, `Width`,
|
|||
|
|
`Height`, `Dir`.
|
|||
|
|
|
|||
|
|
CTPT хранит локальные marker-точки модели. После применения transform такая точка
|
|||
|
|
становится позицией или направлением в мире. Оружие может использовать её для
|
|||
|
|
дула или оси башни, камера -- для привязки обзора, эффект -- для точки появления.
|
|||
|
|
Конкретное назначение определяется именем и consumer-ом, а не одним общим флагом.
|
|||
|
|
Первое 32-битное поле чаще равно `0`; встречаются `0x80000000` и редкий
|
|||
|
|
вариант. До установления точной семантики оно хранится как `flags_raw`.
|
|||
|
|
|
|||
|
|
### NDPR
|
|||
|
|
|
|||
|
|
Проверено 494 NDPR-ресурса и 1 915 записей. Они ссылаются на `animals.rlb`,
|
|||
|
|
`system.rlb`, `static.rlb`, `turrets.rlb`, `weapon.rlb` или используют пустое
|
|||
|
|
имя архива. В 89 записях присутствует связанный эффект. Пустое имя архива
|
|||
|
|
разрешается относительно текущего контекста. Reader хранит ссылку и остальные
|
|||
|
|
параметры раздельно; writer сохраняет исходный порядок.
|
|||
|
|
|
|||
|
|
### EXPL и reference arrays
|
|||
|
|
|
|||
|
|
Проверено 144 ресурса EXPL: 26 используют версию 1, 54 -- версию 2, 64 --
|
|||
|
|
версию 3. Reader выбирает layout по version field и требует точного завершения
|
|||
|
|
payload. Полная field-level семантика всех версий пока не доказана, поэтому
|
|||
|
|
version-specific opaque sections сохраняются.
|
|||
|
|
|
|||
|
|
Отдельная проверенная группа из 585 ресурсов содержит 2 956 однотипных
|
|||
|
|
ссылочных records. Их границы и counts закрыты, однако единое предметное имя
|
|||
|
|
всего семейства не подтверждено всеми consumers. В API безопаснее использовать
|
|||
|
|
нейтральное `ReferenceArray` и конкретизировать назначение на уровне типа entry.
|
|||
|
|
|
|||
|
|
### SUND и CTLD
|
|||
|
|
|
|||
|
|
Два ресурса SUND содержат суммарно 12 ключей. Их следует загружать как параметры
|
|||
|
|
системного объекта, а не как геометрию.
|
|||
|
|
|
|||
|
|
Для CTLD проверено 531 payload. Размеры и сочетания счётчиков сильно различаются,
|
|||
|
|
поэтому parser должен быть версионно- и счётчик-ориентированным, а неизвестные
|
|||
|
|
секции -- храниться в исходном виде.
|
|||
|
|
|
|||
|
|
### TRF, ANI и SKE
|
|||
|
|
|
|||
|
|
В демоверсии обнаружены 5 файлов TRF, 38 preload-записей, 8 ANI-ресурсов и
|
|||
|
|
6 SKE-ресурсов. Все проходят структурный разбор. Эти семейства участвуют в
|
|||
|
|
подготовке компонентов и анимационных или управляющих данных до создания
|
|||
|
|
runtime-объекта.
|
|||
|
|
|
|||
|
|
Поскольку живой корпус невелик, редактор не должен синтезировать новые варианты
|
|||
|
|
этих форматов по догадке. Безопасный режим -- читать доказанные счётчики и
|
|||
|
|
ссылки, предоставлять raw-view неизвестных секций и обеспечивать побайтовое
|
|||
|
|
сохранение неизменённых данных.
|
|||
|
|
|
|||
|
|
### BASE
|
|||
|
|
|
|||
|
|
Проверено 30 BASE-ресурсов; каждый содержит ровно один polygon record и проходит
|
|||
|
|
структурную проверку. BASE payload и ссылка `.bas` в `objects.rlb` выполняют
|
|||
|
|
связанные, но разные роли:
|
|||
|
|
|
|||
|
|
- наличие ссылки `.bas` позволяет registry resolver-у искать одноимённый
|
|||
|
|
`<stem>.msh` в том же архиве;
|
|||
|
|
- сам BASE payload загружается отдельной подсистемой сооружений и не заменяет
|
|||
|
|
MSH geometry.
|
|||
|
|
|
|||
|
|
Resolver не должен интерпретировать bytes BASE как mesh. Writer сохраняет
|
|||
|
|
polygon record и неизвестные поля 1:1, пока полный gameplay-контракт BASE не
|
|||
|
|
подтверждён.
|
|||
|
|
|
|||
|
|
## Правило сохранения
|
|||
|
|
|
|||
|
|
Lossless editor сохраняет неизвестные поля, хвосты фиксированных строк,
|
|||
|
|
служебные bytes, gaps, padding и unindexed regions. Writer пересчитывает только
|
|||
|
|
явно производные значения: размеры, offsets, число записей, сортировочную
|
|||
|
|
перестановку и padding. Такая дисциплина позволяет редактировать известную
|
|||
|
|
часть ресурса, не разрушая данные, смысл которых пока не установлен.
|
|||
|
|
|
|||
|
|
Canonical repack допустим только как явная операция. Он может исключать
|
|||
|
|
неиндексируемые диапазоны, пересортировывать таблицы и пересобирать padding, но
|
|||
|
|
не должен быть побочным эффектом обычного редактирования. Если пользователь
|
|||
|
|
открыл существующий архив и изменил один известный атрибут, все остальные bytes,
|
|||
|
|
не являющиеся производными от этого изменения, должны пройти roundtrip без
|
|||
|
|
потери.
|