aboutsummaryrefslogtreecommitdiff
path: root/docs/tomes/06-behavior.md
diff options
context:
space:
mode:
Diffstat (limited to 'docs/tomes/06-behavior.md')
-rw-r--r--docs/tomes/06-behavior.md207
1 files changed, 171 insertions, 36 deletions
diff --git a/docs/tomes/06-behavior.md b/docs/tomes/06-behavior.md
index ebb3a03..a3fa711 100644
--- a/docs/tomes/06-behavior.md
+++ b/docs/tomes/06-behavior.md
@@ -143,7 +143,7 @@ references каждого record, жёстко ограничивает counts/a
файла, но не таблица семантик: названия opcode/words появятся только после
handler contracts и runtime traces.
-Связь первого header word с dispatch теперь доказана статически: `ai.dll`
+Связь первого header word с dispatch доказана статически: `ai.dll`
создаёт 73 handler pointers в известном порядке и копирует table без
перестановки. По всем 58 GOG packages первый word — индекс `0..72` либо
`0xffff_ffff` sentinel; `fparkan-script` отражает это как typed
@@ -201,16 +201,6 @@ Ghidra 12.1.2 decompile GOG `ai.dll` фиксирует отдельный evalu
typed condition/evaluation layer, но **не** формат `.scr`, размеры инструкций
или связь чисел tag с языковыми операторами.
-Выгрузка воспроизводится без изменения PE:
-
-```powershell
-& 'C:\Tools\ghidra_12.1.2_PUBLIC\support\analyzeHeadless.bat' `
- C:\temp\fparkan-ghidra ai -import 'C:\GOG Games\Parkan - Iron Strategy\ai.dll' `
- -processor x86:LE:32:default `
- -scriptPath C:\Develop\fparkan\tools\ghidra `
- -postScript ExportAiExpressionDispatcher.java -deleteProject
-```
-
### TRF и preload-данные
TRF-файлы проходят структурный разбор. `auto.trf`, `data.trf` и tutorial
@@ -292,9 +282,7 @@ Headless Ghidra 12.1.2 decompile GOG binary подтверждает ABI форÐ
а mode передаётся последним; decompiler не восстанавливает предметные имена
остальных слов. `InitializeSettings` получает `CreateGameSettings()` из
World3D и делает virtual call slot `+0x24` с literal `0x15` и строкой по RVA
-`0x42478`. Reproducible extractor находится в
-`tools/ghidra/ExportControlFunctions.java`; он декомпилирует только эти exports
-в локальном Ghidra project и не изменяет оригинальную DLL.
+`0x42478`.
Именно update methods этих private objects, а не пять exports, остаются
следующим объектом динамической трассировки. Поэтому reference movement в
@@ -330,8 +318,7 @@ interface сразу получает пять virtual calls, связывающ
`+0x158`, `+0x160`, `+0x164`, `+0x168` и `+0x18c`; collision object затем
связывается с `+0x170`. Это достаточное основание хранить будущий Control
component как ordered raw-string/resource provenance, но не для присвоения
-этим строкам смысловых имён до трассировки private update methods. Extractor:
-`tools/ghidra/ExportAniMeshControlCaller.java`.
+этим строкам смысловых имён до трассировки private update methods.
Runtime сохраняет ordered raw Unit DAT records рядом с каждым mission object
draft. Это создаёт проверяемую границу передачи данных от loader-а к будущему
@@ -393,22 +380,36 @@ Collision manager не должен хранить прямую незащищё
### CTLD и physical resources
Реестр прототипов ссылается на `*.ctl`, `*.cpt` и связанные control resources.
-В Части 1 структурно проверен 531 CTLD payload без ошибок. Размеры и пять
-внутренних счётчиков образуют множество вариантов: наиболее частый размер
-392 байта с pattern `(0,0,0,1,0)`, но встречаются блоки от примерно 212 до
-1868 байт и более сложные комбинации.
-
-CTLD является составным count-driven форматом, а не фиксированной struct.
-Parser должен:
-
-- прочитать prefix и все счётчики с проверкой переполнения;
-- вычислить границы секций по их counts;
-- сохранять неизвестные records в typed raw containers;
-- требовать точного завершения payload;
-- не использовать размер одного популярного варианта как универсальный layout.
-
-Полная предметная семантика всех секций ещё не доказана, но существующие файлы
-можно безопасно читать, индексировать и сохранять.
+В заголовке CTLD идут пять `u32`; обозначим первые три counts как `S`, `M` и
+`T`. Native layout задаёт начало control-row table формулой
+`128 + S * (156 + 16 * M) + 4 * S * S`; за ним следуют `T` records по 36 байт.
+В record известны node ID (`i32`, `+0`), два endpoint frame (`f32`, `+4`, `+8`),
+начальный blend (`f32`, `+0x0c`) и raw flags (`u32`, `+0x20`). Остальные поля
+нельзя выводить из этой позовой привязки.
+
+Пример `fr_l_plant.ctl` имеет counts `[14, 0, 5, 11, 13]`: формула даёт
+`row_start = 3096 (0xC18)`, а пять control rows заканчиваются на `+0xCCC`.
+GOG `Control.dll` function RVA `0x9950` читает blend из `+0x0c` и flags из
+`+0x20`. Его branch RVA `0x99C4..0x9A66` применяет flags: при `flags & 0x1`
+blend один раз переносится через границу — из значения выше `1` вычитается
+`1`, к значению ниже `0` прибавляется `1`; без этого флага blend ограничивается
+`[0, 1]`. Затем `flags & 0x2` инвертирует значение (`1 - blend`). Константы
+`Control.dll+0x3B188 = 1.0` и `+0x3B18C = 0.0` подтверждают границы этих
+сравнений.
+
+Control в call site RVA `0x99B5` передаёт в AniMesh через slot `+0x30` пару A
+`[-1, -1]` и пару B из endpoint полей строки. В call site `0x9A7B` slot `+0x28`
+сохраняет нормализованный blend по `+0x114` и weight `1` по `+0x118`. AniMesh
+update RVA `0x8BF2..0x8C76` вычисляет
+`frame = (1 - blend) * endpoint_a + blend * endpoint_b`; update RVA `0x12560`
+выбирает B-state из-за weight `1`. В static preview строки с `flags & 0x4 != 0`
+пропускаются до проверки float полей, поскольку endpoint поля в них могут быть
+sentinel `-1`. Повторные node ID сохраняют исходный порядок, поэтому последняя
+включённая строка задаёт кадр узла. Привязки сохраняются отдельно для каждого
+экземпляра компонента, даже когда `PreparedVisual` разделяется через cache.
+Это даёт начальную позу preview, но не запускает игровой Control, AI или полный
+runtime animation path. Остальные
+секции CTLD shape records и contact solver не входят в эту интерпретацию.
### Terrain queries и movement handoff
@@ -452,10 +453,8 @@ exports; RVA всех пяти exports изменились. Форматы и c
сохранились, но точное physical/collision behavior нельзя считать побайтно тем
же.
-CTLD-корпус расширен с 531 до 623 payload. Новых framing errors не найдено;
-большинство общих CTLD изменено вместе с переработанными моделями. Это
-подтверждает count-driven parser, но не закрывает предметную семантику shape
-records и contact solver.
+Покадровая привязка выше описывает начальную позу компонента. Она не задаёт
+семантику CTLD shape records и не восстанавливает contact solver.
Differential test обеих частей должен воспроизводить движение без препятствий,
slope following, pair collision, timing collision event и удаление объекта в
@@ -569,6 +568,142 @@ Ngi32 создаёт низкоуровневый DirectSound backend. `services
`ISoundServer`. Game, Terrain и FX работают уже через эти интерфейсы:
воспроизводят 2D/3D sources, меняют volume и связывают listener с camera.
+### Погода, осадки и звуковые события
+
+Погода для игрока — это одновременно движущиеся точки на экране и звуковой
+фон. Расписание `sky.ske` говорит, в какой момент действует дождь, снег или
+молния, а `sky.wea` назначает имена материалов. Система окружения каждый
+кадр превращает это состояние в два списка: мировые частицы для renderer-а и
+звуковые переходы для audio backend-а.
+
+```text
+sky.ske + sky.wea
+ -> состояние погоды
+ -> EnvironmentFrame
+ -> Particle / Lightning (renderer)
+ -> StartLoop, SetLoopVolume, StopLoop, OneShot (sound)
+```
+
+Такое разделение нужно для понятной границы ответственности. Система погоды
+решает, **что** произошло и где находится источник. Renderer решает, как
+нарисовать прозрачный квадрат или молнию. Audio backend разрешает архив и
+имя, создаёт источник звука и сравнивает его с текущим listener.
+
+У осадков есть объём перед наблюдателем. Эталонные границы имеют глубину
+`2..50`, половины углов `0.65` и `0.4875` радиана. Для текущей камеры:
+
+```text
+half_y = tan(vertical_fov / 2) * 50
+half_x = half_y * aspect_ratio
+```
+
+Размер осадков получает тот же camera query, что и native `Terrain`: ширина
+viewport делится на горизонтальный FOV в радианах.
+
+```text
+horizontal_fov = 2 * atan(tan(vertical_fov / 2) * aspect_ratio)
+precipitation_scalar = viewport_width / horizontal_fov
+rain_size = precipitation_scalar * 0.0065
+snow_size = precipitation_scalar * 0.0195
+```
+
+Число точек получает масштабирование по отношению текущего объёма к
+эталонному и округляется к ближайшему чётному целому:
+
+```text
+N = round_even(clamp(current_volume / reference_volume, 0, 1)
+ * density * intensity * 1000)
+```
+
+В рабочем эмиттере `density` равна единице. Точка сначала появляется в
+локальных координатах этого объёма, затем получает мировую позицию. Дождь
+движется с мировым вектором `[0.5, 0, -60]`, снег — `[0.5, 0, -4]`.
+Поворот камеры меняет видимую область и проекцию, но не вращает эти векторы.
+Когда точка пересекает грань, она переводится в локальные координаты,
+циклически переносится на противоположную грань и возвращается в мир.
+
+Поэтому источник звука и положение частицы должны храниться в мировых
+координатах. У дождя хвостом экранной полосы становится предыдущая мировая
+позиция; у снега остаётся квадрат в текущей позиции. Scalar вычисляется из
+projection и реального viewport каждого кадра, поэтому отдельная настройка
+размера не нужна.
+
+Дождевой loop следует жизненному циклу состояния:
+
+| Событие | Действие audio backend |
+| --- | --- |
+| `StartLoop` | открыть объявленный sample и начать пространственный loop |
+| `SetLoopVolume` | сохранить loop и применить новую интенсивность как громкость |
+| `StopLoop` | остановить текущий loop |
+| `OneShot` | создать отдельный источник и воспроизвести sample молнии один раз |
+
+Интенсивность между ключами `sky.ske` интерполируется, поэтому `SetLoopVolume`
+может приходить на каждом кадре. Имя из расписания сохраняется. Если указано
+только `atm_rain1.wav`, архив остаётся пустым и audio owner использует
+библиотеку миссии; запись `archive/name` задаёт архив явно. Ресурс загружается
+лениво и кэшируется после проверки, чтобы не читать все возможные погодные
+звуки при запуске миссии.
+
+Молния использует отдельный таймер. Для интенсивности `I` и случайного `U` из
+15-битного диапазона задержка имеет вид
+
+```text
+delay_ms = round_even((1 - min(I, 0.95)) * 60000 * U)
+```
+
+После срока выбираются мировые X и Y из `LightningBounds`, а Z копируется из
+границ. Затем объект ждёт 6000 миллисекунд перед новой попыткой. Визуальный
+контракт передаёт renderer-у material и numeric body opcode 3; native
+descriptor `[40, 40, 600]` начинается на `sampled_z + 300`. При нулевом
+локальном смещении его концы находятся на `sampled_z` и `sampled_z + 600`.
+Opcode 1 в это же время отдельно обновляет point light; он не задаёт размеры
+или UV quad. Звуковой `OneShot` использует ту же позицию, а его opcode 2
+параметры `min_distance=100`, `max_distance=1500`, `frequency_ratio=1`
+проходят в spatial source. Renderer и audio backend применяют numeric FX
+поля и lifetime из заголовка эффекта, а CPU не подменяет их собственной
+шириной или fade-кривой.
+
+Подробные поля `EnvironmentFrame`, правила wrap и границы CPU-модуля собраны
+в [справочнике эффектов окружения](../reference/environment-effects.md).
+
+### Ambient variations и переход день/ночь
+
+`ambient_music_loop` запускает `THEME` сразу после открытия миссии. Вариации
+не выбираются последовательным счётчиком: audio owner получает `dt_seconds`
+как приращение времени кадра и после строгого условия `elapsed > delay`
+выбирает один sample.
+Первый положительный tick поэтому запускает первую вариацию, а задержка между
+следующими попытками равна `10 + rand() % 10` секунд.
+
+В `ambient_music_variation` поддерживаются три независимых пула:
+
+| Пул | Ключи | Когда выбирается |
+| --- | --- | --- |
+| default | `DEFAULT_VARIATION1..n` | когда отсутствуют оба пула `DAY` и `NIGHT` |
+| day | `DAY_VARIATION1..n` | длина базового RGB активного небесного объекта больше `1.1` |
+| night | `NIGHT_VARIATION1..n` | длина базового RGB активного небесного объекта не больше `1.1` |
+
+Базовый RGB передаётся до camera-dependent glare и берётся у первого активного
+небесного объекта. Если существует хотя бы один day/night пул, выбранный
+пустой пул остаётся пустым: он не заменяется default или противоположным пулом.
+`LIBRARY` у `ambient_music_variation` может отличаться от библиотеки theme;
+если поле отсутствует, используется библиотека loop.
+
+Индекс выбирается двумя 16-битными состояниями Iron3D:
+
+```text
+a = (a << 1) xor b
+b = (b >> 1) xor a
+index = b % pool_length
+```
+
+При длине пула больше одного предыдущий индекс отбрасывается одной или более
+повторными выборками. Последний индекс сохраняется при переходе между day и
+night; для пустого пула sample не создаётся, но следующий таймер продолжает
+работать. При пустом пуле native сбрасывает индекс в `-1`. Пауза окна сохраняет
+логическое состояние таймера и индекса, поэтому
+возобновление не перескакивает на случайную вариацию.
+
Публичные функции Ngi32:
```text