diff options
Diffstat (limited to 'docs/tomes/06-behavior.md')
| -rw-r--r-- | docs/tomes/06-behavior.md | 301 |
1 files changed, 272 insertions, 29 deletions
diff --git a/docs/tomes/06-behavior.md b/docs/tomes/06-behavior.md index 48f4405..3a4e51c 100644 --- a/docs/tomes/06-behavior.md +++ b/docs/tomes/06-behavior.md @@ -168,8 +168,11 @@ handler лишь инициирует execution context (`+0x50 = 1`); его ga младшие 32 бита, что даёт `0`. Значение `10001` также проходит дальше: оригинал показывает предупреждение для чисел выше `10000`, но не отменяет создание AI. -Rust исполняет только подтверждённую форму этого действия. Поддержанные записи -применяются к состоянию лишь после успешного прохода всего первого `Init`. +Rust исполняет подтверждённый поднабор первого `Init`: действие `Handler(19)` +и формулы из одного атома либо двух атомов с бинарным `+` или `-`. Здесь атом — +число или имя переменной. Поддержанные шаги сразу обновляют временную копию +varset, поэтому следующая формула читает уже записанные значения. К состоянию +клана записи попадают лишь после успешного прохода всего первого `Init`. Если встретилась неподдержанная форма, preview сохраняет пакет, сообщает `Unsupported` и оставляет значения по умолчанию; при полной загрузке та же форма становится ошибкой. Это политика Rust для ограниченного исполнителя, а не правило @@ -177,8 +180,62 @@ Rust исполняет только подтверждённую форму эÑ событие считается исполненным без записей. Остальные события пока не образуют исполняемую VM. +В `Init` встречаются и отдельные присваивания результатов формул. Их запись +выбирает строку формулы по порядковому номеру из соседнего `.fml`; этот файл +относится к конкретной базе сценария, а строки после нужного номера сохраняют свои позиции. +Для первой кампанийной миссии `Mission.01` (`tut1_pl2.fml`) строка 0 записывает +нули в `df0..df3`; в миссии `Single.01` (`scr_pl_1.fml`) строка 0 записывает +нули в `dT8` и `dT9`. +Клан 1 той же миссии использует первые девять строк `scream.fml`: восемь +проверенных значений попадают в `DWORD` с усечением дробной части, а `0.8` +сохраняется как число `float`. Например, если `source` равен `10`, выражение +`source + 2.0` записывает `12`; следующее `target - source` читает новое значение +`target` и получает `2`. Каждая операция работает с `f32`: значение ячейки +`DWORD` сначала читается как беззнаковое число и переводится в `f32`, а результат +сохраняет тип целевой ячейки. Поддерживаются только один или два атома и один +бинарный `+` либо `-`; произвольная грамматика формул и остальная VM не +восстановлены. В отдельной точечной проверке вычисления и записи оригинальной +DLL и Rust совпали для 13 выбранных формул. Эта сверка проверяет только +эти значения и не запускает исходную миссию. Тест полной загрузки подтверждает, +что `Mission.01` проходит этот `Init`; у `Single.01` следующий неподдержанный +`Handler(43)` встречается в clan 1, записи 10. Никакое значение не подставляется +вместо этого действия. + ### Первый запуск сценария +Перед настройкой первого шага каждый AI получает начальный массив DWORD. В +`iron3d.dll` SHA-256 +`d1dc4ea8535e2069b0a05c9e8be2724dd438ba44cca7a83007d8b2e50f9e35fa` +`FUN_100392e0` (RVA `0x392e0`) повторяет вызов слота AI `+0x38` столько раз, +сколько задаёт `second_resource.tag` этой строки TMA. Этот слот ведёт в +`ai.dll` SHA-256 +`c19cd6cd31cd2896f8c4ba0f0e700dd3408aec8182daaf4863d98a7d233a2abc`: +метод RVA `0x2010` передаёт коллекцию при `AI+0x83c` в `FUN_10002980` +(RVA `0x2980`), а она добавляет один DWORD через `FUN_100048f0` (RVA +`0x48f0`). Каждый вызов копирует `0xffffffff`; входное слово не меняется. +Поэтому тег `3` даёт `[0xffffffff, 0xffffffff, 0xffffffff]`. Это число +начальных ячеек, а не назначение трёх ID. + +Изолированная x86-проверка вызовом оригинальной `ai.dll` подтвердила пары +«число слотов/ёмкость» `0/0`, `1/17`, `17/17` и `18/34`. Все добавленные слова +равны `0xffffffff`, а входное значение, контрольные поля, память вне заголовка +массива и указатель стека сохраняются. Это подтверждает добавление начального +слова, но не работу последующих обработчиков. + +Rust хранит такой снимок в отдельном `MissionAiStartupState` для каждого TMA-клана, +а не в пакете сценария. При полной загрузке (`Full`) создаётся запись для каждого +клана, в том числе если у него нет пути сценария или пути таблицы исследований. Счётчик +берётся из сырых битов `second_resource.tag` как `u32` у обеих форм записи — +`Standard` и `Spatial`; нулевой счётчик даёт существующее состояние с пустым +списком. Статический режим предпросмотра не создаёт состояний AI. + +Загрузка отклоняет суммарный объём свыше 1 000 000 DWORD и резервирует память +проверяемой операцией; это защита Rust от чрезмерного выделения памяти, а не +ограничение оригинальной игры. Rust сохраняет начальные слова, но не +восстанавливает работу последующих обработчиков и не назначает этим словам ID +объектов. Этот массив также не является списком +`BehaviorDistributorState.registered_objects`. + До регулярной работы Behavior настраивает расписание своего первого шага. Он берёт текущее 32-битное время игровой очереди и назначает проверку через три интервала по 64 мс, добавляя небольшую случайную задержку от 0 до 63 мс. Эта @@ -196,9 +253,10 @@ SuperAI для записей кланов, затем обновляет Behavi от повторного входа; при занятой защите вызов пропускается, но одноразовая метка всё равно устанавливается. В полной загрузке Rust вызывающая сторона передаёт значение времени очереди; после регистрации объектов загрузчик последовательно -подготавливает каждый сценарный пакет и выполняет только подтверждённые действия -`Handler(19)` из первого `Init`. Статический просмотр не запускает Behavior, но -может сохранить подтверждённые записи `Init` в сценарном varset. +подготавливает каждый сценарный пакет и выполняет восстановленные действия +`Handler(19)` и поддержанные формульные присваивания из первого `Init`. Статический +просмотр не запускает Behavior, но может сохранить эти записи `Init` в сценарном +varset. Это полезное начало сценария, но ещё не целая игровая жизнь AI. Текущий runtime не воспроизводит аргумент обратного вызова в `AI+0x3fc`, защиту от повторного @@ -243,6 +301,25 @@ Rust-тест повторно читает лицензированную миÑ TMA-записями и проверяет реальные `ObjectHandle`, но помечен `#[ignore]`, чтобы не требовать оригинальных ресурсов от остальных разработчиков. +После подготовки Control-таблицы можно разобрать и её начальную группировку для +Behavior. Важны текущая метка записи и её порядковый номер в общей таблице: +метка `1` начинает пустую группу, метка `2` становится кандидатом последней +группы, а метка `30` записывает строку в отдельный список той же группы. +Кандидат, встретившийся до первого заголовка, остаётся в списке без группы; +ранняя метка `30` игнорируется. Например, +для текущих меток `[2, 30, 1, 30, 2]` получаются незагруппированный кандидат +`[0]` и группа с заголовком `2`, кандидатом `[4]` и строкой с меткой `30` под +порядковым номером `[3]`. Все номера начинаются с нуля и отсчитываются по общей +Control-таблице, а не по ключам устройств или записям CTLD. + +Полная загрузка Rust сохраняет это начальное состояние рядом с подготовленными +Control-таблицами по исходному TMA ID и `ObjectHandle`. Если таблица недоступна, +проекция сохраняет причину ошибки; доступная пустая таблица означает пустую +группировку. При предварительном просмотре список группировок не создаётся. Здесь +разбирается только подготовленный порядок текущих Control-дескрипторов. Проекция +не устанавливает, когда список присоединяется к живому Behavior, как он затем +обновляется и как работает полный игровой цикл. + Список распределителя хранит не только участников, но и долю каждого. При добавлении двух объектов оба получают по половине общего веса. Если добавить третий, движок оставляет долю новой записи около одной трети, а оставшийся вес @@ -468,16 +545,117 @@ Tag выбирает конкретное правило. Для tag 1 запиÑ ### TRF и preload-данные -TRF-файлы проходят структурный разбор. `auto.trf`, `data.trf` и tutorial -variants имеют сигнатуру [NRes](../reference/nres.md) и содержат большие -таблицы имён игровых прототипов: оружия, башен, сооружений и других объектов. -Также найдены preload-записи, ANI и SKE resources. +TRF-архивы используют несколько потребителей; один подтверждённый путь ведёт +из `Handler(43)` к вычислению стоимости исследования. В проверенной сборке GOG +`CalcFullResearchCost` из `ArealMap.dll` получает таблицу, загруженную через +`MisLoad.dll`. Для этого пути `TRF0` содержит строки по десять сырых `u32`, +`TRF1` — байты флагов, `TRFB` — пары «смещение имени и номер строки», а +`TRF6` — пул строк с нулевыми окончаниями. Слово 8 строки связывает её с +`TRFB` через младшие 16 бит; старшая половина сохраняется как неизвестное +значение. `TRF2` и `TRF3` образуют упорядоченные списки предварительных +исследований, которые читает этот путь стоимости. `TRF4`/`TRF5` образуют +отдельную пару таблиц, но `CalcFullResearchCost` её не использует. Непрозрачные +`TRF7`–`TRFA` остаются в архиве и требуются загрузчику. + +Модуль `fparkan-assets` разбирает этот профиль в `ResearchTrfDocument` и +вычисляет пару результатов для одного Unit DAT: + +```rust +let table = ResearchTrfDocument::decode(archive_bytes)?; +let cost = calc_full_research_cost(&candidate, &table)?; +``` + +Расчёт проходит компоненты Unit DAT в порядке дерева. Компонент с нулевым +`kind` завершает весь свой участок вместе с потомками. Для остальных ключ +ресурса ищется среди имён `TRFB`; повтор строки добавляется лишь один раз. +После прямых компонентов алгоритм обходит список `TRF2`/`TRF3` в его исходном +порядке, углубляясь сразу в найденные предварительные исследования. Общая +таблица посещений не даёт повторно добавлять узел и завершает циклы. Два бита +флагов `TRF1` управляют включением строки и обходом её зависимостей; остальные +сырые биты и слова не получают игровых имён без отдельного доказательства. + +Результат первого native-выхода подписан в отладочной строке как ore и +суммирует слово 1 `TRF0`, второй подписан как power и суммирует слово 0. Каждое +сложение сохраняется как `f32` в порядке найденной последовательности. В +принятом наборе из 23 синтетических тестов оригинальных DLL проверялись +`CalcFullResearchCost` и два AI-предиката; именно вызовы `CalcFullResearchCost` +подтвердили порядок обхода, дедупликацию, циклы, флаги и отсечение дерева. +Две проверки суммы показали чувствительность округления: одна и та же сумма +`1, 2^24, -2^24` даёт `0`, а в другом порядке `2^24, -2^24, 1` даёт `1`. + +Эти результаты относятся к синтетическому исследовательскому контексту; +штатный Handler(43) и запуск миссии здесь не проверялись. + +Владелец начальной записи исследования — индекс текущего прохода по кланам. +В GOG-сборке `iron3d.dll!FUN_1005ff40` (RVA `0x5ff40`) проходит записи кланов +и вызывает для каждой `FUN_100392e0` (RVA `0x392e0`), которая создаёт AI; +`ai.dll!CreateSuperAI` (RVA `0xf710`) выделяет объект и вызывает конструктор +по RVA `0x1000`, не проверяя наличие сценарного пути. Для исследовательского +пути нужны отдельный флаг включения и непустой `second_resource.path`; наличие +сценарного пакета условием не является. Индекс клана сохраняется в +`descriptor[0]` и переносится в контекст, чтобы связать запрос с AI-данными +того же клана. + +Это значение отдельно от аргумента 1 `AddNewObjectToGame`, берущегося из +`GetNetPlayerNum`, и от TMA-тега `second_resource`, задающего число вызовов +AI-слота `+0x38`. Передача поля подтверждена по статическому коду; выполнение +Handler(43) и штатный запуск миссии остаются непроверенными. Технические +сведения и адреса исходных сборок приведены в [границах знания](../appendices/knowledge-boundaries.md). + +Полный Rust-загрузчик принимает явный `MissionStartupInput`: время очереди +и отдельный `research_creation_gate`. Режим миссии и имя файла этот +флаг не заменяют. При включённом флаге загрузчик использует уже имеющееся +`MissionAiStartupState` каждого TMA-клана, даже если у него нет сценарного +пакета; путь берётся из второго ресурса как C-строка до первого NUL. Пустой +путь и выключенный флаг пропускают чтение, а не-ASCII байты дают явную ошибку. +Предпросмотр тоже не читает TRF. Клан индексируется по порядку в TMA, а тег +`second_resource` задаёт число слотов AI, не владельца. Если несколько кланов +указывают на один файл, каждый получает отдельно декодированную таблицу со +своими строками TRF0. + +```rust +MissionStartupInput { + queue_time_ms: 100_000, + research_creation_gate: true, +} +``` -По содержимому, порядку загрузки и consumers TRF с высокой вероятностью -предоставляет AI/сценарному слою заранее подготовленную таблицу типов и -связанных данных. Framing и имена подтверждены corpus-ом, но полная семантика -каждой TRF-записи ещё не закрыта. Имена должны разрешаться через тот же -resource registry, что и миссионные объекты. +Это сохраняет декодированные таблицы и связь с владельцами для следующих +этапов. Оно ещё не создаёт полный исследовательский контекст оригинала, не +строит AI-каталог и не запускает `Handler(43)`; эти части пока не +восстановлены. + +Отдельная frozen-матрица непосредственно вызвала chooser GOG `ai.dll` SHA-256 +`c19cd6cd31cd2896f8c4ba0f0e700dd3408aec8182daaf4863d98a7d233a2abc`: функция +`FUN_10010f90` (RVA `0x10f90`) прошла три guard-case и все 15 проверок. +Конструктор находится в RVA `0x10480`, raw DAT parser — `0x102c0`, +предикаты — `0x115f0` и `0x11530`. Матрица передавала кандидатов в заданном +порядке. Обёртка `Handler(43)` по RVA `0xd54a` и загрузчик AI-каталога по RVA +`0x104e0` не вызывались. Cost принадлежит `ArealMap.dll` SHA-256 +`a25f9254ebb339d70e2e07c8121950191e9584ca0858d8e9c2e2b6c671edf6ea`, +`CalcFullResearchCost` RVA `0x16280`; для сравнения x87 был явно задан как +`0x007f`. + +`ResearchCandidateSet` держит упорядоченный каталог, выбранный индекс, байт +scan-status и флаг `loaded` каталога; TRF-контекст передаётся отдельно. +При наличии каталога и контекста set-status начинает со значения `1`, затем +перед чтением каждого entry со status `0` становится `0`. Ненулевой entry +пропускается. Первый предикат требует info words 1 и 2, соответствующие битам +TRF1 0 и 2; второй требует words 0 и 2, то есть биты 1 и 2. Второй status +сохраняется в entry, а cost вызывается даже при status `1`. Выбор записывается +сразу, когда second predicate ложен и ore строго ниже текущего минимума, +изначально `f32::from_bits(0x4b18967f)`. Равенство сохраняет первого, NaN индекс не меняет. + +В Rust ошибка VFS-чтения и невалидный DAT пропускают entry; неизвестный TRF- +ключ даёт false predicate. Ошибка стоимости `EmptyResearchTable` тоже +пропускает entry и сохраняет его второй status. Остальные ошибки таблицы и +`UnsupportedNonAsciiLookup` возвращаются явно, причём уже сделанные изменения +индекса и status остаются. Общий Rust-предикат может закончить обход раньше +native callback `0x11530`, который продолжает накапливать неиспользуемые float +sums. Первый проход уже проверил enabled keys в той же неизменной таблице, +поэтому подтверждается эквивалентность состояния и выбора для этого входа, +но не callback-trace parity. Это не подтверждение цены на живой миссии или +порядка AI-каталога. ### Стабильность AI-слоя @@ -883,21 +1061,41 @@ descriptor tag `19` и положительным selector-0 ответом; пÑ его vtable slot `+0x10` статически указывает на общий getter RVA `0x21d00`, но отдельный aggregate oracle этот класс не запускал. Rust хранит начальный scale factory `9` и предоставляет `query_loaded_mission_control_resource` для selector -`1`/`2` и `set_loaded_mission_control_multiplier` для setter-а `1`. Вызовы -сначала проверяют живой `ObjectHandle`, затем обращаются к таблице этого объекта -из полной загрузки миссии. Синтетические тесты проверяют границы пустой таблицы, -нулевого знаменателя, неподдержанных factory tags и чисел за диапазоном -нормального `f32`; лицензированный тест сверяет реальные descriptor tags и -получает для `min_l_n1.dat` сумму `16` и отношение `1` до setter-а. - -В этом API пока реализованы только getter selectors `1`/`2` и setter `1`. +`1`, `2` и `15`, а также `set_loaded_mission_control_multiplier` для setter-а +`1`. Вызовы сначала проверяют живой `ObjectHandle`, затем обращаются к таблице +этого объекта из полной загрузки миссии. Синтетические тесты проверяют границы +пустой таблицы, нулевого знаменателя, отсутствующих объектов фабрик и чисел за +диапазоном нормального `f32`; лицензированный тест сверяет реальные descriptor +tags и получает для `min_l_n1.dat` сумму `16` и отношение `1` до setter-а. + +Сырой селектор `0x0f` соединяет два ранее созданных объекта владельца: у строки +с `factory_tag = 9` берётся коэффициент и его текущий множитель, а у строки с +`factory_tag = 21` — шесть текущих `coefficient_bits`. `INTO` может поменять +`descriptor.tag`, не заменяя объект фабрики, поэтому query выбирает последние +строки по `factory_tag`, а не по текущему tag. Если любой объект фабрики +отсутствует, ответ — `None`. Иначе шесть коэффициентов складываются с индекса `5` +к `0` с округлением до `f32` после каждого сложения; результат умножается на +selector `0` фабрики `9` (`coefficient_bits[0] * class9_selector0_scale_bits`). +Общие `multiplier_bits`, `node_provider_status` и `native_word_50_bits` в этой +формуле не участвуют. + +Лицензионная проверка загружает AutoDemo с явно переданными +`MissionStartupInput { queue_time_ms: 100_000, research_creation_gate: false }` +и вызывает `query_loaded_mission_control_resource(..., 15)` для исходных +объектов `0..5`; для `6` и `7` Rust возвращает `None`, потому что не хватает как +минимум одного объекта фабрики. Native-биты для `0..5` приведены в технической +главе. Проверка сверяет результат исходного геттера на снимке входных полей, +подготовленном Rust. Время и флаг заданы самим тестом; они не доказывают +начальный режим запуска AutoDemo в оригинале и не являются захватом его фабрик +или состояния объекта Control. + Отдельный native setter selector `17`, tag-specific методы прочих factory tags, их внешние эффекты и вызов из регулярного шага Behavior здесь не моделируются. Числа должны оставаться конечными и нормальными либо точным нулём; произведения -и отношения за поддерживаемым диапазоном `f32` возвращают явную ошибку, а не -подменяются нулём или `None`. При проверке диапазона Rust оценивает операцию в -`f64`, затем выполняет её в `f32`; это ограниченная проверка диапазона, а не -эмуляция полного x87 accumulator-а. +и суммы за поддерживаемым диапазоном `f32` возвращают явную ошибку, а не +подменяются нулём или `None`. При проверке диапазона Rust сначала оценивает +границы в `f64`, затем выполняет каждую операцию в `f32`; это сохраняет +пошаговое округление этого getter-а, но не эмулирует весь x87 execution state. Отдельная ветка того же GOG `AniMesh.dll` показывает, когда создаётся Behavior для host object. В `FUN_10002ff0` (RVA `0x2ff0`, SHA-256 @@ -941,8 +1139,31 @@ NDP, а `FUN_1000a100` (RVA `0xa100`) добавляет их в общую та последовательность provider-строк: её длина берётся из NDP, она не дополняется до числа узлов MSH и не участвует во второй перенумерации ключа. -Так этот селектор включён в цепочку добычи руды оригинала. В той же сборке -`Behavior.dll` (SHA-256 +Индексный selector `0x400` выдаёт значение выбранной строки provider как есть. +Сначала общий индекс устройств выбирает дескриптор, затем его ключ выбирает +строку NDP; это разные номера. Результатом становится текущее type-1 значение +этой строки, а не запас `CurrentOre`, предел или значение после умножения на +коэффициент и `M`. Например, provider-строка с `0.625` возвращает `0.625`, а +записанный в ней ноль тоже остаётся успешным результатом. Rust использует уже +подготовленную таблицу и сообщает отдельную ошибку, если provider-таблица или +строка по ключу недоступна. Этот запрос читает снимок состояния и сам не меняет +таблицу и не запускает её обновление. + +У правила есть два разных подтверждения. Разбор AutoDemo нашёл восемь таблиц +Control и 100 устройств; все 100 значений по ключам устройств в этом корпусе +равны `1.0`, поэтому Rust-проверка подтверждает реальные ключи и доступность +строк, но сама по себе не различает выбор по ключу и по номеру устройства. +Отдельная x86-проверка исходного `Control.dll` включала шесть вызовов на +синтетической таблице, где значения строк были заданы вручную и различались. +Например, при номере устройства `2`, ключе `1` и нулевом слове +`device+0x50` оригинал вернул `0.625` из выбранной строки. Точные биты ответов +приведены в [техническом разборе](../appendices/script-vm.md#индексный-control-getter-сырой-селектор-0x400). +Этот опыт не создавал оригинальные factory-объекты и не исполнял полную миссию: +полный интерфейс IID `0x204`, интеграция Behavior и игровое поведение остаются +непроверенными. + +В цепочке добычи руды оригинал использует описанный выше selector `0x300`. +В той же сборке `Behavior.dll` (SHA-256 `ddfbdbde07832358fe6e35e01c8dba9b58e7313789498b6049ed51d4ed9baf99`) `FUN_100186b0` (RVA `0x186b0`) перечисляет устройства типа `0x1a`, а `FUN_100198e0` (RVA `0x198e0`) суммирует успешные запросы IID `0x204` с selector @@ -1354,7 +1575,7 @@ offsets 119–121. В `fr_l_gener.ctl` найдено 35 строк type 4, вк `f_gener_ball` с offset 85. Control.dll `+0x8B10` передаёт указатель текущей записи диспетчеру `+0x2D4B0`. -Для tags `2`, `3`, `4`, `5`, `8`, `10`, `21` и `27` подтверждён общий читатель +Для tags `2`, `3`, `4`, `5`, `8`, `10`, `17`, `21` и `27` подтверждён общий читатель `+0x21D50`: таблица виртуальных методов tag `2` (`+0x3CBD8`) вызывает его через обёртку `+0x29650`, остальные перечисленные vtable указывают прямо на него в позиции `+0x1C`. Читатель потребляет @@ -1365,6 +1586,28 @@ Control.dll `+0x8B10` передаёт указатель текущей запÐ а также отказ при оборванном завершающем NUL. Это подтверждает структурный формат, но не назначает смысл всем коэффициентам устройств. +Путь записи с меткой `17` проверен на GOG `Control.dll` с SHA-256 +`bcb86318089028548a178b03a21accdce15df0cc85ddd91674cf3c2f55012c6e`: +ветвь `0x11` фабрики `+0x2D4B0` вызывает конструктор `+0x24760`, который +устанавливает указатель на виртуальную таблицу `0x1003C840`. Ячейка `+0x1C` +этой таблицы ссылается прямо на общий читатель `+0x21D50`. В ресурсах +`weapon.rlb:bm_m_01.ctl` и `weapon.rlb:bm_l_01.ctl` таблица привязок +заканчивается на смещении `0x120`, где начинается запись с меткой `17`. +Общий префикс размером `0xB0` байт включает саму метку; счётчик по `+0xAC` и +слово длины в этих двух ресурсах равны нулю, поэтому указатель чтения +переходит на `0x1D4`. Там начинаются `0x54` байта заголовка таблиц эффектов; +таблицы содержат соответственно `4`, `2`, `2` и `2` строки по 100 байт и +заканчиваются на `0x620`. Записи из реестра объектов `bm_m_01` и `bm_l_01` +при обычной подготовке ресурсов сохраняют явно указанные списки точек CPT +(`bm_m_a.cpt` и `bm_l_a.cpt`) и таблицы узлов NDP (`bm_m_01.ndp` и +`bm_l_01.ndp`); в первой таблице эффектов находятся три строки type 4. +Синтетический тест проверяет непустые массивы значений и строки у записи с +меткой 17 и следующей записи с меткой 4, затем переход к таблице эффектов. +Если считать 17 неизвестной, чтение остановится перед +ней; общий разбор позволяет пройти запись и прочитать следующие таблицы, +оставляя слова tag 17 числами без толкования. Обработчики этой метки и их +игровое поведение ещё не восстановлены. + Resolver прототипов хранит явную registry-ссылку на `.cpt` отдельно от ссылки на `.ctl`. Если строки type 4 разобраны и явная ссылка на CPT есть, подготовка читает ровно указанные archive/name; имя CPT не выводится из имени CTL. В CPT |
