1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
|
# 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 без
потери.
|