aboutsummaryrefslogtreecommitdiff
path: root/docs/specs/nres.md
blob: bb318234365030c87fdccc660ca9656ccf15181b (plain) (blame)
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
# NRes

`NRes` — базовый контейнер ресурсов движка Parkan: Iron Strategy.  
Страница фиксирует формат на диске и runtime-контракт чтения/поиска/сохранения в высокоуровневом виде, без привязки к внутренним адресам и именам из дизассемблера.

Связанная страница:

- [RsLi](rsli.md)

## 1. Назначение

`NRes` используется как универсальный архив:

- 3D-модели (`*.msh`, `*.rlb`);
- текстуры (`Texm`);
- материалы (`MAT0`);
- эффекты (`FXID`);
- миссионные и служебные ресурсы.

Формат поддерживает:

- чтение;
- поиск по имени;
- редактирование (add/replace/remove);
- полную пересборку архива.

## 2. Общий layout файла

```text
[Header: 16]
[Data region: variable, 8-byte aligned chunks]
[Directory: entry_count * 64, всегда в конце файла]
```

Критично: каталог всегда расположен в конце файла.

## 3. Заголовок (16 байт)

Все значения little-endian.

| Offset | Size | Type | Значение |
|---:|---:|---|---|
| 0 | 4 | char[4] | `NRes` |
| 4 | 4 | u32 | `0x00000100` (версия 1.0) |
| 8 | 4 | i32 | `entry_count` (должен быть `>= 0`) |
| 12 | 4 | u32 | `total_size` (должен быть равен фактическому размеру файла) |

Производные значения:

- `directory_size = entry_count * 64`;
- `directory_offset = total_size - directory_size`.

Ограничения:

- `directory_offset >= 16`;
- `directory_offset + directory_size == total_size`.

## 4. Запись каталога (64 байта)

| Offset | Size | Type | Поле |
|---:|---:|---|---|
| 0 | 4 | u32 | `type_id` |
| 4 | 4 | u32 | `attr1` |
| 8 | 4 | u32 | `attr2` |
| 12 | 4 | u32 | `size` (размер payload) |
| 16 | 4 | u32 | `attr3` |
| 20 | 36 | char[36] | `name_raw` (C-строка) |
| 56 | 4 | u32 | `data_offset` |
| 60 | 4 | u32 | `sort_index` |

### 4.1. Имя ресурса (`name_raw`)

Контракт:

- максимум 35 полезных байт + NUL;
- допускается ровно один терминатор внутри 36-байтового поля;
- имя сравнивается регистронезависимо по ASCII-правилу (`A..Z` -> `a..z`).

Для writer/editor:

- запрещено писать NUL внутри полезной части имени;
- запрещены имена длиной > 35 байт.

### 4.2. Диапазон данных (`data_offset`, `size`)

Для каждой записи:

- `data_offset >= 16`;
- `data_offset + size <= directory_offset`.

Практически (канонический writer): каждый payload начинается с 8-байтного выравнивания.

## 5. Таблица сортировки (`sort_index`)

`sort_index` задает перестановку «отсортированный список -> исходный индекс записи».

Пусть:

- `entries[i]` — i-я запись каталога в исходном порядке;
- `P` — массив индексов `0..entry_count-1`, отсортированный по `entries[idx].name` (ASCII case-insensitive).

Тогда в канонической записи:

- `entries[i].sort_index = P[i]`.

Это именно таблица для бинарного поиска по имени, а не «ранг текущей записи».

## 6. Поиск по имени

Алгоритм поиска:

1. Выполнить бинарный поиск по диапазону `i in [0, entry_count)`.
2. На шаге `i` взять `target = entries[i].sort_index`.
3. Сравнить искомое имя с `entries[target].name` (ASCII case-insensitive).
4. При совпадении вернуть `target`.

Fail-safe поведение:

- если `sort_index` некорректен (выход за диапазон), реализация должна перейти на линейный fallback по всем записям;
- fallback использует то же ASCII case-insensitive сравнение.

## 7. Каноническая пересборка архива

Канонический writer выполняет:

1. Пишет заглушку заголовка (16 байт).
2. Пишет payload всех записей в текущем порядке.
3. После каждого payload добавляет 0-padding до кратности 8.
4. Пересчитывает `sort_index` через сортировку имен.
5. Дописывает каталог (`entry_count * 64`).
6. Пересчитывает и записывает `total_size`.

Итоговый файл должен удовлетворять всем ограничениям из разделов 3–5.

## 8. Режим `raw` (совместимость инструментов)

Для служебных инструментов допускается `raw_mode`:

- любой бинарный файл трактуется как один «сырой» ресурс;
- возвращается одна запись (`name = RAW`, `data_offset = 0`, `size = len(file)`).

Этот режим не является форматом `NRes` на диске, это только режим открытия.

## 9. Контрольные инварианты

Минимальный набор проверок при чтении:

1. `magic == "NRes"`.
2. `version == 0x100`.
3. `entry_count >= 0`.
4. `header.total_size == file_size`.
5. Каталог находится в конце файла.
6. Для каждой записи диапазон данных не пересекает каталог.
7. Имя корректно C-терминировано и не длиннее 35 байт.

Минимальный набор проверок при записи:

1. Все имена <= 35 байт и без внутренних NUL.
2. `sort_index` формирует валидную перестановку `0..N-1`.
3. Все паддинги между payload состоят из нулевых байт.
4. `total_size` равен фактической длине выходного файла.

## 10. Эмпирическая проверка на retail-корпусе

Валидация на полном наборе `testdata/Parkan - Iron Strategy`:

- найдено `120` архивов `NRes`;
- roundtrip `unpack -> repack -> byte-compare`: `120/120` совпали побайтно;
- критических расхождений формата не обнаружено.

Инструмент:

- `tools/archive_roundtrip_validator.py`

## 11. Статус покрытия и что осталось до 100%

Закрыто:

- формат заголовка/каталога;
- правила поиска;
- каноническая пересборка;
- строгие инварианты валидатора;
- побайтовый roundtrip на retail-корпусе.

Осталось до полного 100% архитектурного покрытия движка:

1. Формальная семантика `attr1/attr2/attr3` для всех типов ресурсов (частично вынесена в профильные страницы `msh`, `material`, `texture`, `fxid`, `terrain`).
2. Полная спецификация поведения при не-ASCII именах (в реальных игровых архивах используется ASCII-практика; для Unicode-коллации движок не документирован).
3. Полная спецификация платформенных гарантий атомарной записи (формат данных закрыт, но OS-уровневые гарантии замены файла зависят от платформы и файловой системы).
## 12. Специализация `objects.rlb`

Хотя `objects.rlb` формально является обычным `NRes`, его payload имеет отдельный семантический контракт:

- запись каталога соответствует одному объектному прототипу;
- payload записи - массив фиксированных ссылок `ObjectRef64` (`archive_name[32] + resource_name[32]`);
- runtime-резолв меша выполняется через эти ссылки, а не через имя entry `*.msh` внутри `objects.rlb`.

Это означает, что `objects.rlb` должен рассматриваться не как архив мешей, а как реестр привязок между mission/unit-ключами и фактическими ресурсами.

См. детальную страницу:

- [Object registry (`objects.rlb`)](object-registry.md)