feat: complete map viewer scene and static CTL pose preview
Complete the interactive mission viewer with environment rendering, audio events, dynamic shadows, free-flight camera controls, and per-component CTL pose sampling.
This commit is contained in:
@@ -10,7 +10,7 @@ messages, teleports, задачи, research и campaign transitions. Точки
|
||||
и `briefing.cfg`.
|
||||
|
||||
`.scr` — binary package с version checks, symbol/event sections и offsets;
|
||||
полная opcode grammar не доказана. Его внешний framing теперь читает
|
||||
полная opcode grammar не доказана. Его внешний framing читает
|
||||
`fparkan-script`: первый little-endian `u32` является числом required opcode
|
||||
handlers, второй — числом event records. Каждый event хранит `name_len`,
|
||||
`name_len + 1` raw bytes с обязательным NUL, opaque event word и count вложенных
|
||||
@@ -108,24 +108,21 @@ unsupported result, а не «примерный» game command.
|
||||
записи `0x100059f0` вызывает `0x1000f920`, а Ghidra 12.1.2 декомпилирует эту
|
||||
функцию как пустой `return`. Следовательно, найденные `<base>_Start` и
|
||||
`<base>_Continue` только кэшируются в scheduler state; их фактический consumer
|
||||
находится в отдельном позднем update path. Воспроизводимый read-only extractor:
|
||||
`tools/ghidra/ExportAiVmHandler2Dispatch.java`.
|
||||
находится в отдельном позднем update path.
|
||||
|
||||
Corpus priority теперь измерен, а не предполагается: во всех 58 GOG `.scr`
|
||||
имеются 6 087 instruction records, из них 3 992 sentinel; самый частый
|
||||
non-sentinel selector — `Handler(30)`, 246 records. Его VA `0x1000c266`
|
||||
читает первые два reference words активной instruction, разрешает каждый
|
||||
через varset (`0x10002d30` и `0x10013570`) и вызывает внешний callback с
|
||||
тремя `u32`: `(0, first, second)`. Callback не принадлежит `ai.dll`: его
|
||||
Во всех 58 GOG `.scr` имеются 6 087 instruction records, включая 3 992
|
||||
sentinel. Самый частый non-sentinel selector — `Handler(30)`, 246 records.
|
||||
Его VA `0x1000c266` читает первые два reference words активной instruction,
|
||||
разрешает каждый через varset (`0x10002d30` и `0x10013570`) и вызывает внешний
|
||||
callback с тремя `u32`: `(0, first, second)`. Callback не принадлежит `ai.dll`: его
|
||||
кладёт десятый argument экспортного `CreateSuperAI`. Тот же callback встречен
|
||||
у `Handler(57)` с первым word `2` и у отдельного lifecycle path с первым word
|
||||
`1`; предметная семантика этих modes ещё не доказана. В частности, это пока
|
||||
не основание назвать Handler(30) сообщением, приказом или UI opcode. Точный
|
||||
text-to-varset resolver расположен за wrapper `0x10011ea0` в
|
||||
`0x100174a0`. Воспроизводимые exports: `ExportAiVmHandler30.java`,
|
||||
`FindAiVmHandler30Callback.java`, `ExportAiVarSetLoader.java`.
|
||||
`0x100174a0`.
|
||||
|
||||
Следующий pass восстанавливает эту индексацию. `0x100174a0` добавляет каждый
|
||||
`0x100174a0` добавляет каждый
|
||||
recognized source declaration в encounter order как 48-byte record; GOG shared
|
||||
`varset.var` не содержит `STRING(...)`, поэтому его 231 numeric `VAR` entries
|
||||
образуют точно это index space. `0x10013570` возвращает `DWORD` record kind
|
||||
@@ -134,10 +131,9 @@ capture. Полный GOG scan всех 246 Handler(30) instructions показ
|
||||
operand references: все 492 in-range и указывают на `DWORD`. Поэтому
|
||||
`VarSet::resolve_handler30` уже materializes точный opaque callback command
|
||||
`(mode=0, first, second)` для данного corpus path, но явно отклоняет float,
|
||||
out-of-range и incomplete instructions вместо silent coercion. Extractors:
|
||||
`ExportAiVarSetParser.java`, `ExportAiVarSetU32Resolver.java`.
|
||||
out-of-range и incomplete instructions вместо silent coercion.
|
||||
|
||||
Следующий static pass закрывает equality/update policy. Identity ровно равна
|
||||
Identity ровно равна
|
||||
`(slot0 word, slot4 IEEE-754 bits, slot5 IEEE-754 bits)`, поэтому `-0.0` и
|
||||
`+0.0` различаются. Новый 100-byte record получает slot1 в поле `+0x14`,
|
||||
slot2 одновременно в `+0x24/+0x28`, slot3 в `+0x2c` и slot6 в `+0x0c`. При
|
||||
@@ -147,7 +143,7 @@ slot2 одновременно в `+0x24/+0x28`, slot3 в `+0x2c` и slot6 в `+
|
||||
изолированную часть как `Handler2RecordScheduler`; он не выполняет bytecode,
|
||||
не назначает игровых имён и не делает event lookup за original VM.
|
||||
|
||||
На границе mission runtime выбранный TMA clan `first_resource` теперь
|
||||
На границе mission runtime выбранный TMA clan `first_resource`
|
||||
материализуется как отдельный `MissionScriptBundle`: loader нормализует
|
||||
`<base>.scr`, декодирует его тем же bounded reader-ом и публикует immutable
|
||||
package вместе с clan provenance. Это именно wiring входных данных, не VM
|
||||
@@ -206,9 +202,9 @@ contains two initialized SuperAI entries `(500, 752, 0)` and `(728, 449, 1)`;
|
||||
the Rust loader reports `script_init_states=2` and `script_varset_states=2`.
|
||||
|
||||
`GetSuperAI` returns element `n` of the 64-pointer global table at preferred
|
||||
`ai.dll + 0x55398` for `n <= 63`. The read-only
|
||||
`tools/capture-ai-init.ps1` probe observed the running GOG AutoDemo values
|
||||
`(500, 752, 0)` for entry 0 and `(728, 449, 1)` for entry 1 at fields
|
||||
`ai.dll + 0x55398` for `n <= 63`. A read-only capture of the running GOG
|
||||
AutoDemo process observed `(500, 752, 0)` for entry 0 and `(728, 449, 1)` for
|
||||
entry 1 at fields
|
||||
`(+0x80, +0x84, +0x7c)`. These values are integral samples, not a rounding
|
||||
profile.
|
||||
|
||||
@@ -216,12 +212,7 @@ The Rust reader exposes `VarSet::resolve_handler19`. It accepts the already
|
||||
converted first two words and the third raw word, produces three typed writes,
|
||||
and rejects missing, out-of-range, or non-`DWORD` targets. The runtime only
|
||||
binds it to the proven creation/anchor path above; it does not guess the
|
||||
remaining script event semantics. The associated Ghidra scripts are
|
||||
`ExportAiVmHandler19.java`, `ExportAiVmHandler19Setter.java`,
|
||||
`ExportAiVmHandler19SetterCallee.java`, and `ExportAiGetSuperAi.java`.
|
||||
The creation and conversion boundaries are reproducible with
|
||||
`ExportAiCreateSuperAi.java`, `ExportAiSuperAiConstructor.java`, and
|
||||
`ExportAiFtol.java`.
|
||||
remaining script event semantics.
|
||||
|
||||
### Runtime Handler(30) operand binding
|
||||
|
||||
@@ -256,12 +247,7 @@ proves the lookup and one-shot/repeat split, not the UI/message subject or the
|
||||
semantics of either resource ID; Rust therefore retains command `1` as
|
||||
`Unhandled`.
|
||||
|
||||
Reproduce the callback and the command-one consumers with
|
||||
`capture-ai-init.ps1`, `ExportIron3dAiCallback.java`,
|
||||
`ExportIron3dAiCallbackCommand1.java`, and
|
||||
`ExportIron3dAiCallbackCommand1Dispatch.java`.
|
||||
|
||||
Runtime now applies only this recovered branch as
|
||||
Runtime applies only this branch as
|
||||
`apply_loaded_script_host_callback`: `(0, 0, 0)` transitions a loaded mission
|
||||
to `Failed`, `(0, 0, 1)` transitions it to `Completed`, and a repeated target
|
||||
state is a no-op just as the Iron3D guards require. The effect is deliberately
|
||||
@@ -284,9 +270,7 @@ opaque callback slots; state `3` does the same except word `3` is preserved.
|
||||
Both then write `+0x18`. Every other state simply writes the state word.
|
||||
`VarSet::resolve_handler8` emits a `Handler8StateChange` with the caller-owned
|
||||
live record index, resolved state, and explicit reset kind. It does not invent
|
||||
the table owner, the pre-reset helper, or callback semantics. Reproduce the
|
||||
evidence with `ExportAiVmHandler8.java`, `ExportAiVmHandler8Callees.java`, and
|
||||
`ExportAiVmHandler8Transitions.java`.
|
||||
the table owner, the pre-reset helper, or callback semantics.
|
||||
|
||||
### Handler(15): typed target-call boundary
|
||||
|
||||
@@ -314,8 +298,7 @@ with that record and the third word. Its zero/non-zero result becomes
|
||||
return value remain unproven. Accordingly `VarSet::resolve_handler15` only
|
||||
materializes a type-checked `Handler15Invocation` and `Handler15TargetPayload`;
|
||||
it never executes the opaque target call. Missing, out-of-range, wrong-type,
|
||||
and unobserved-mode inputs are explicit errors. Reproduce the static evidence
|
||||
with `tools/ghidra/ExportAiVmHandler15.java`.
|
||||
and unobserved-mode inputs are explicit errors.
|
||||
|
||||
## Готовность
|
||||
|
||||
|
||||
@@ -18,6 +18,13 @@ FParkan воспроизводит его работу на Rust и Vulkan. На
|
||||
7. [Работа над движком](tomes/07-implementation.md): код, тесты и проверка результата.
|
||||
8. [Устройство оригинальной программы](tomes/08-evidence.md): DLL, адреса и конфигурация.
|
||||
|
||||
В справочнике можно быстро сверить [NRes](reference/nres.md),
|
||||
[RsLi](reference/rsli.md), [TMA](reference/tma.md), [MSH](reference/msh.md),
|
||||
[материалы](reference/materials.md), [Texm](reference/texm.md),
|
||||
[кадр рендера](reference/render-frame.md), [атмосферу и небо](reference/atmosphere.md)
|
||||
и [эффекты окружения](reference/environment-effects.md). Таблица DLL находится
|
||||
в [описании оригинальных модулей](reference/original-binaries.md).
|
||||
|
||||
В [глоссарии](appendices/glossary.md) собраны термины, а в
|
||||
[открытых вопросах](appendices/knowledge-boundaries.md) — ещё не восстановленное
|
||||
поведение. Описание алгоритма оригинала и возможности текущего приложения
|
||||
|
||||
@@ -0,0 +1,236 @@
|
||||
# Атмосфера и небо
|
||||
|
||||
Атмосфера Parkan задаётся двумя связанными ресурсами: `sky.ske` хранит
|
||||
расписание, цвета и параметры, а `sky.wea` сопоставляет позиционные строки с
|
||||
материалами. Ресурсы сначала разбираются в `TypedAtmosphere` и `SkyMaterials`,
|
||||
затем на каждом кадре из расписания получается `AtmosphereFrame`. Модуль
|
||||
`fparkan-fx::sky` превращает этот кадр в данные для рендерера: постоянную
|
||||
геометрию купола, динамические цвета вершин, небесные слои, положение
|
||||
светил, туман и нижнюю границу освещения.
|
||||
|
||||
## Игровые сутки и смена дня и ночи
|
||||
|
||||
Вызов `TypedAtmosphere::sample(real_seconds)` принимает секунды от начала
|
||||
повторяющегося расписания. Дорожки идут в порядке файла и занимают свои
|
||||
реальные длительности; после последней дорожки время возвращается к началу.
|
||||
Внутри выбранной дорожки игровой день всегда проходит от 00:00 до 24:00:
|
||||
|
||||
```text
|
||||
track_seconds = real_seconds_in_track
|
||||
day_seconds = track_seconds / track_duration * 86400
|
||||
```
|
||||
|
||||
Время ключа вычисляется по часам и минутам даты. Нативный конструктор
|
||||
игнорирует сохранённое поле секунд, поэтому для планировщика используется:
|
||||
|
||||
```text
|
||||
duration = (end_hour * 60 + end_minute) * 60
|
||||
key_time = floor(duration * (key_hour * 60 + key_minute) * 60 / 86400)
|
||||
```
|
||||
|
||||
Сэмплер ищет предыдущий и следующий ключ во всём расписании. Переход через
|
||||
конец дорожки или конец цикла интерполируется так же, как переход внутри
|
||||
дорожки. `SkeDate::seconds_of_day()` сохраняет все три компонента даты для
|
||||
инструментов; оно не меняет формулу реального времени расписания.
|
||||
|
||||
День и ночь в самом формате не являются отдельным флагом. Их вид получается
|
||||
из интерполированных цветовых параметров купола и состояния объектов солнца и
|
||||
луны. Ключи с kind `0` и `1` запускают и останавливают небесное светило. Для
|
||||
активного интервала модуль восстанавливает рождение объекта: ищет ближайший
|
||||
предыдущий ключ `SunStart`, следующий соответствующий `SunStop`, вычисляет
|
||||
реальное время начала и длительность, включая переход через цикл, и сохраняет
|
||||
эти значения в `SunBirth`.
|
||||
|
||||
Имя в первой фиксированной строке определяет объект. Для строки `sun`
|
||||
нативный код использует углы `[90, 30]` градусов и строку материала 3; для
|
||||
`moon` — `[0, 50]` и строку 4. Начальный вектор орбиты строится по формуле
|
||||
`[cos(theta), 0, -sin(theta)]`, где
|
||||
|
||||
```text
|
||||
theta = (clamp((now - birth_start) / lifetime, 0, 1) * 1.2 - 0.1) * pi
|
||||
```
|
||||
|
||||
Затем к нему применяется матрица рождения. У солнца и луны независимые
|
||||
интервалы и независимые направления: при отсутствии активной луны нельзя
|
||||
подменять её направление противоположностью солнца. `SunFrame::color` — это
|
||||
единственный primary directional RGB. Второй источник получает направление
|
||||
`-SunFrame::direction` и RGB из `SunSample::packed[1]`; оба источника для
|
||||
активного объекта находятся в `SkyFrame::directional_lights` (сначала солнце,
|
||||
затем луна). Экранная геометрия использует `-SunFrame::direction`, а
|
||||
`world_center` размещает её по нативной формуле относительно camera
|
||||
translation и половины far plane. Материал, строка и состояние объекта
|
||||
берутся из его `SunBirth` и таблицы `sky.wea`.
|
||||
|
||||
## Купол
|
||||
|
||||
Оригинальный `CSky` строит конечный купол, а не кубическую skybox. Значения
|
||||
конструктора, подтверждённые в Terrain, такие: высота `10000`,
|
||||
`theta_max = pi / 4`, вертикальный масштаб `1`, `16` азимутальных секторов и
|
||||
`5` колец. Радиус вычисляется как
|
||||
|
||||
```text
|
||||
radius = height * 0.5 / sin(theta_max * 0.5)^2
|
||||
```
|
||||
|
||||
Вершина с индексом 0 находится в зените `[0, 0, height * z_scale]`.
|
||||
Остальные вершины идут в порядке `azimuth`, затем `ring`; для них
|
||||
|
||||
```text
|
||||
phi = azimuth / azimuth_count * 2*pi
|
||||
theta = (ring + 1) / rings * theta_max
|
||||
x = radius * sin(theta) * sin(phi)
|
||||
y = radius * sin(theta) * cos(phi)
|
||||
z = (radius * cos(theta) + height - radius) * z_scale
|
||||
```
|
||||
|
||||
На сектор приходится один треугольник веера и по два треугольника на каждую
|
||||
пару соседних колец. Текстурные координаты трёх исходных стадий используют
|
||||
масштабы `[1, 15, 3]` для `position.x / radius` и `position.y / radius`.
|
||||
Цвет зенита и первого пояса берётся из palette 12. Следующие пояса используют
|
||||
группы palette 8..11, 4..7 и 0..3 с интерполяцией по азимуту.
|
||||
|
||||
Позиции, signed normal bytes, UV и индексы создаются один раз через
|
||||
`SkyGeometryConfig::mesh`/`SkySystem::new`. Последующие кадры меняют только
|
||||
цвета вершин методом `SkyMesh::update_colors`; индексы и topology не
|
||||
пересоздаются. Ошибка конфигурации возвращается как `SkyGeometryError`, а
|
||||
ошибка входного кадра — как `SkyUpdateError`; они не превращаются в пустой
|
||||
буфер. Рендерер может загрузить `SkySystem::mesh()` в статический
|
||||
vertex/index buffer и обновлять только небольшой цветовой диапазон.
|
||||
|
||||
Нормали купола повторяют native `Terrain47BFF` contract: каждый компонент
|
||||
хранится как signed `i8`, округляется режимом nearest-even и умножается на
|
||||
`127.0`. `SkyVertex::normal_vector()` декодирует эти bytes делением на `127`;
|
||||
зенит имеет `[0, 0, 127]`. Вершинный backend должен передавать signed byte
|
||||
representation без трактовки его как unsigned `0..255`.
|
||||
|
||||
## Облака, звёзды и спрайты
|
||||
|
||||
`sky.wea` — позиционная таблица. В стандартном AutoDemo строки имеют роли:
|
||||
|
||||
| Строка | Роль | Имя в AutoDemo |
|
||||
|---:|---|---|
|
||||
| 0 | фон/туманность | `ENV_NEBULA_0` |
|
||||
| 1 | звёзды | `ENV_STARS` |
|
||||
| 2 | облака | `ENV_CLOUDS` |
|
||||
| 3 | солнце | `ENV_SUN_3` |
|
||||
| 4 | луна | `ENV_MOON` |
|
||||
| 5 | первый flare | `ENV_FLARE_00` |
|
||||
| 6 | второй flare | `ENV_FLARE_01` |
|
||||
| 7 | снежинка | `SNOWFLAKE` |
|
||||
| 8 | капля дождя | `RAIN_DROP` |
|
||||
|
||||
Это наблюдаемые строки, а не глобальные имена, на которые можно полагаться в
|
||||
миссии. `SkyMaterials::parse` сохраняет числовой id и имя каждой строки;
|
||||
миссия может заменить любой материал. `SkyLayerKind` и `SkyLayerFrame` дают
|
||||
роль, позиционную строку, активность, intensity, UV stage и фазу времени.
|
||||
|
||||
Нативный порядок купола находится в `SkyFrame::passes`: экранный gradient,
|
||||
nebula row 0 со stars row 1 как второй texture (material mode 4), dome
|
||||
gradient и clouds row 2. Для nebula/stars/clouds используются UV scales
|
||||
`1/15/3`; clouds получают translation Z `-5000` относительно камеры и
|
||||
directional RGB из `sky.packed[0]`. Это четыре фиксированных pass, а не три
|
||||
независимых цветных слоя.
|
||||
|
||||
Цвет первого screen-gradient и последнего пояса вычисляет
|
||||
`SkyFrame::screen_gradient(yaw_radians, glare_rgb_delta)`. Native `CSky` сначала
|
||||
округляет `(yaw + pi) * 180 / pi` режимом nearest-even, выбирает сектор
|
||||
`(floor(degrees / 90) - 2) mod 4`, а затем смешивает соседние цвета
|
||||
`SkySample::colors[0..4]`. Остаток сектора умножается на точный коэффициент
|
||||
из PE `65F64` (`0x3C360B61`, примерно `0.01113567`), alpha результата равна
|
||||
`255`. `glare_rgb_delta` — входной RGB delta от light manager: для каждого
|
||||
канала это `max(current_primary_light_rgb - base_sun_rgb, 0)`. Каждый компонент
|
||||
затем преобразуется так: `d <= 1 -> d*0.8`, `1 < d <= 3 -> d*0.1 + 0.7`,
|
||||
`d > 3 -> 1`.
|
||||
`SkyGradientFrame::horizon_floor` задаёт RGB minimum для dome palette, а
|
||||
`clamp_dome_color` применяет его, сохраняя alpha исходного цвета.
|
||||
|
||||
Солнце, луна и две строки flare являются направленными экранными слоями.
|
||||
Нативный quad использует UV
|
||||
`[[.005,.005],[.005,.995],[.995,.995],[.995,.005]]`. Полуразмеры sun/moon в
|
||||
пикселях вычисляет `sprite_half_size_pixels`: `viewport_width / horizontal_fov
|
||||
* .325 * .5 * values[0/1]`, где `values` — первые два tail float SKE.
|
||||
Цвет sprite и его alpha берутся из `SunSample::packed[0]`, отдельно от
|
||||
directional `SunFrame::color`.
|
||||
|
||||
`SkyFrame::sun_optics` принимает projected sun, viewport, camera forward и
|
||||
результат world-ray visibility. Он применяет native 15-degree glare cone и
|
||||
возвращает фиксированные 12 `FlareQuad`; flare slots активны только при
|
||||
видимом солнце, `length(primary RGB) > 1.1` и cone amount `<= .1`. При cone
|
||||
`> .1` остаётся только glare boost. Renderer отвечает за сам projection и
|
||||
occlusion query.
|
||||
|
||||
Дождь, снег и молния используют те же состояния расписания, но их camera-local
|
||||
геометрия и звуки обновляются `EnvironmentSystem`. Для активного интервала
|
||||
ресурсы берутся только из ключа, который его запустил. Интенсивность дождя,
|
||||
снега и молнии — четвёртый trailing float; цвет активной погоды использует
|
||||
RGB palette 12 с минимумом `80/255` на канал и alpha `150/255`.
|
||||
|
||||
## Туман и свет
|
||||
|
||||
Нативное обновление `CSky` записывает две дальности и один packed RGB.
|
||||
`FogFrame` повторяет этот контракт:
|
||||
|
||||
```text
|
||||
fog.start = sky.values[0] * 700
|
||||
fog.end = sky.values[1] * 700
|
||||
fog.color = RGB(sky.packed[1]) / 255
|
||||
```
|
||||
|
||||
Поля не скрывают порядок исходных параметров: значения сохраняются в
|
||||
`SkySample`, а преобразование выполняется только в `fog_frame`. Vulkan path
|
||||
передаёт цвет и дальности в frame uniforms; vertex shader вычисляет линейный
|
||||
коэффициент по расстоянию до камеры, а fragment shader смешивает RGB материала
|
||||
с fog color в ветвях combiner, которые используют туман. Конкретный draw state
|
||||
может обходить это смешивание.
|
||||
|
||||
Цвет primary света солнца — RGB palette 12, нормированный делением на 255 и
|
||||
умноженный на trailing parameter 2. `SunFrame::direction` — нормированный
|
||||
вектор после матрицы рождения и направление primary light; sprite/world
|
||||
geometry использует его противоположность. Второй directional RGB берётся из
|
||||
packed-параметра 1 без дополнительного умножения intensity. Когда объект
|
||||
неактивен, sampled поля сохраняются, но оба light slot и sprite inactive.
|
||||
|
||||
В native `CShade` sampled RGB из состояния атмосферы также устанавливает
|
||||
глобальную нижнюю границу освещения. Для каждого RGB-канала она объединяется
|
||||
операцией `max` с накопленным цветом материала. Поэтому `SkyFrame` отдаёт
|
||||
`lighting_floor` из того же sampled packed RGB, а владелец renderer передаёт
|
||||
его в глобальный light state. Фиксированный ambient-цвет в этом месте меняет
|
||||
ночной уровень и не соответствует цепочке Terrain → CShade → Ngi32.
|
||||
|
||||
## Бинарный формат `sky.ske`
|
||||
|
||||
Файл little-endian, версия 5. В начале находятся `i32 marker = -1`,
|
||||
`u32 version = 5` и число дорожек. Дорожка содержит `u32 version = 1`, число
|
||||
ключей, две даты по 32 байта и ключи. Дата состоит из восьми `u32`; для
|
||||
планировщика значимы слова 3, 4 и 5 (часы, минуты, секунды), а остальные слова
|
||||
сохраняются без интерпретации.
|
||||
|
||||
Ключ имеет `u32 version = 3`, дату, raw kind, opaque word, четыре packed ARGB
|
||||
цвета, два `f32`, пятнадцать packed ARGB цветов, шесть length-prefixed строк,
|
||||
четыре trailing `f32` и список length-prefixed ссылок. Строка хранится как
|
||||
`u32 byte_length` и ровно столько байт без завершающего NUL. Неизвестный kind
|
||||
сохраняется как `AtmosphereKind::Unknown`, как и opaque слова/строки.
|
||||
|
||||
В конце находятся дата и два opaque `u32`. Декодер ограничивает количества и
|
||||
размеры строк, отклоняет NaN/Infinity во float-полях и требует точного конца
|
||||
файла. Трейлер выбирает исходную дорожку и позицию час/минута; метод
|
||||
`initial_offset_seconds()` складывает длительности предыдущих дорожек и
|
||||
применяет ту же формулу floor.
|
||||
|
||||
Из интерполированного ключа `SkySample` формируется в исходном порядке:
|
||||
|
||||
```text
|
||||
colors = [header[1], header[2], header[0], header[3],
|
||||
palette[0], palette[3], palette[1], palette[2],
|
||||
palette[4], palette[7], palette[5], palette[6], palette[8]]
|
||||
values = header_values
|
||||
packed = [palette[11], palette[13]]
|
||||
```
|
||||
|
||||
Солнце и луна используют palette 12 и trailing parameter 2 для RGB,
|
||||
parameters 0 и 1 сохраняются как raw values, а packed-параметры светила —
|
||||
palette 10 и 14. Kind `0/1` управляет солнцем и луной по имени, `3/4` —
|
||||
дождём, `5/6` — снегом, `8/9` — молнией; kind `2` и `7` меняют
|
||||
интерполируемое состояние. Поля, для которых в native renderer ещё нет
|
||||
проверенного назначения, остаются raw в Rust API и не получают придуманных
|
||||
значений по умолчанию.
|
||||
@@ -0,0 +1,203 @@
|
||||
# Эффекты окружения
|
||||
|
||||
Погода в Parkan состоит из двух связанных частей. Файл `sky.ske` задаёт
|
||||
расписание: когда начинается дождь или снег, как меняются интенсивность и цвет,
|
||||
какие имена ресурсов принадлежат активному интервалу. Таблица `sky.wea` задаёт
|
||||
материалы, которыми эти осадки рисуются. `fparkan-fx::environment` соединяет
|
||||
оба входа с текущей камерой и возвращает один `EnvironmentFrame`.
|
||||
|
||||
```text
|
||||
sky.ske
|
||||
-> AtmosphereFrame: время, цвет, интенсивность, ссылки на ресурсы
|
||||
sky.wea
|
||||
-> SkyMaterials: имена материалов строк 7 (снег) и 8 (дождь)
|
||||
камера + EnvironmentSystem
|
||||
-> EnvironmentFrame: мировые частицы, экранные квадраты, гром и дождевой loop
|
||||
```
|
||||
|
||||
Модуль не владеет графическим устройством или звуковой картой. Renderer
|
||||
разрешает имя материала и отправляет геометрию в прозрачный проход, а audio
|
||||
backend разрешает архив и имя звука. Это позволяет одной и той же модели
|
||||
погоды работать в preview и в игровом цикле.
|
||||
|
||||
## Входы и границы
|
||||
|
||||
Для расписания используется такой вызов:
|
||||
|
||||
```rust
|
||||
let frame = environment.update_atmosphere_with_materials(
|
||||
dt_seconds,
|
||||
&atmosphere_frame,
|
||||
&sky_materials,
|
||||
camera,
|
||||
);
|
||||
```
|
||||
|
||||
`AtmosphereFrame` передаёт ссылки из стартового ключа активного интервала.
|
||||
Для дождя первая ссылка используется как звук фонового loop. Строка вида
|
||||
`archive/name` разделяется на архив и имя. У простой строки `name` архив
|
||||
остаётся пустым: звуковой владелец подставляет библиотеку, выбранную миссией.
|
||||
Имя не заменяется глобальным именем из AutoDemo.
|
||||
|
||||
`SkyMaterials` хранит разобранную таблицу, поэтому строки 7 и 8 являются
|
||||
положением в формате исходной игры, а не зашитыми названиями. В AutoDemo там
|
||||
находятся `SNOWFLAKE` и `RAIN_DROP`; миссия может передать другие имена. Если
|
||||
активной погоде не назначен материал, частицы для неё не создаются.
|
||||
|
||||
На входной границе конечные значения приводятся к безопасному диапазону:
|
||||
`dt_seconds` и интенсивность неотрицательны, интенсивность и каждый компонент
|
||||
цвета ограничены единицей, а нечисловые значения заменяются нулём. Камера
|
||||
получает конечную ортонормированную основу, FOV, aspect ratio и размер
|
||||
viewport от владельца кадра.
|
||||
|
||||
## Объём и движение осадков
|
||||
|
||||
Исходный эмиттер хранит прямоугольный объём в координатах камеры. Его
|
||||
эталонные параметры таковы:
|
||||
|
||||
```text
|
||||
near = 2
|
||||
far = 50
|
||||
half_angle_x = 0.65
|
||||
half_angle_y = 0.4875
|
||||
```
|
||||
|
||||
Для эталонной камеры:
|
||||
|
||||
```text
|
||||
half_x = tan(half_angle_x) * far
|
||||
half_y = tan(half_angle_y) * far
|
||||
origin = [near, -half_x, -half_y]
|
||||
extent = [far - near, 2 * half_x, 2 * half_y]
|
||||
```
|
||||
|
||||
Текущий FOV и aspect ratio пересчитывают две боковые грани по той же глубине:
|
||||
|
||||
```text
|
||||
half_y = tan(vertical_fov / 2) * far
|
||||
half_x = half_y * aspect_ratio
|
||||
```
|
||||
|
||||
Количество частиц зависит от объёма и интенсивности. Вспомогательная
|
||||
формула использует округление FISTP к ближайшему чётному целому:
|
||||
|
||||
```text
|
||||
N = round_even(
|
||||
clamp(current_volume / reference_volume, 0, 1)
|
||||
* density
|
||||
* intensity
|
||||
* 1000
|
||||
)
|
||||
```
|
||||
|
||||
Текущий эмиттер передаёт `density = 1`. Публичный
|
||||
`PrecipitationVolume::particle_count_for` оставляет этот множитель явным для
|
||||
владельца, который хранит собственную плотность.
|
||||
|
||||
Порядок обновления частицы важен:
|
||||
|
||||
1. при создании три 15-битных значения детерминированного генератора
|
||||
заполняют локальный объём;
|
||||
2. локальная точка переводится через основу камеры в мировую позицию;
|
||||
3. мировая позиция интегрируется с мировым вектором скорости;
|
||||
4. для проверки границ позиция временно переводится обратно в локальные
|
||||
координаты, и к каждой оси применяется floor-based wrap;
|
||||
5. после wrap точка снова переводится в мир, а её хвост сбрасывается на голову.
|
||||
|
||||
Скорости не принадлежат системе координат камеры:
|
||||
|
||||
```text
|
||||
rain = [ 0.5, 0, -60 ]
|
||||
snow = [ 0.5, 0, -4 ]
|
||||
```
|
||||
|
||||
Это мировые векторы. Поворот камеры меняет область появления, wrap и
|
||||
проекцию, но не вращает уже движущуюся каплю вокруг наблюдателя. В
|
||||
`EnvironmentPrimitive::Particle` `position`, `velocity`, `world_head` и
|
||||
`world_tail` имеют мировые координаты.
|
||||
|
||||
Размер частиц состоит из коэффициента класса и масштаба погодного объекта:
|
||||
|
||||
```text
|
||||
rain_size = scalar * 0.0065
|
||||
snow_size = scalar * 0.0195
|
||||
```
|
||||
|
||||
`scalar` вычисляется из той же проекции, которую передаёт владелец кадра:
|
||||
|
||||
```text
|
||||
horizontal_fov = 2 * atan(tan(vertical_fov / 2) * aspect_ratio)
|
||||
scalar = viewport_width / horizontal_fov
|
||||
```
|
||||
|
||||
Это соответствует запросу размера native camera (`Terrain` primary vtable
|
||||
slot `+6c`): ширина `RECT` делится на горизонтальный FOV в радианах. Поэтому
|
||||
`Camera::with_projection(...).with_viewport([width, height])` должен получать
|
||||
реальный viewport каждого кадра; отдельного setter-а масштаба нет.
|
||||
|
||||
`ScreenBillboard` содержит мировые голову и хвост, их проекцию и четыре NDC
|
||||
угла с UV. `half_size` остаётся native-величиной в пикселях, а обе глубины
|
||||
хранятся отдельно; renderer не смешивает эти единицы. Дождь использует
|
||||
предыдущую мировую голову как хвост и строит полосу перпендикулярно экранному
|
||||
направлению движения. Снег остаётся квадратом в текущей голове, использует
|
||||
исходную таблицу знаков и минимальный depth fade `0.1`. Renderer сам решает,
|
||||
как загрузить этот квадрат и как смешать его материал.
|
||||
|
||||
## Молния и звуковые события
|
||||
|
||||
`decode_env_lightning_fxid` извлекает из FXID длительность, ссылки opcode 3 и
|
||||
2, первые четыре числа opcode 1 и полный body opcode 1. Имена не подменяются
|
||||
глобальным whitelist-ом: материал и звук принадлежат выбранному FXID.
|
||||
`LightningEffect` устанавливается через `set_lightning_effect`; без него
|
||||
погодный таймер не создаёт визуал или звук.
|
||||
|
||||
Активный объект использует следующий таймер. При интенсивности `I` сначала
|
||||
применяется `min(I, 0.95)`, затем для случайного `U` из 15-битного диапазона
|
||||
вычисляется задержка:
|
||||
|
||||
```text
|
||||
delay_ms = round_even((1 - min(I, 0.95)) * 60000 * U)
|
||||
```
|
||||
|
||||
После достижения срока выбираются X и Y из переданных `LightningBounds`, а Z
|
||||
берётся без изменения. Если границы не заданы, используется позиция камеры.
|
||||
Следующая попытка разрешена через 6000 миллисекунд. Длительность уже
|
||||
созданного FX берётся из его заголовка. Renderer получает native descriptor
|
||||
`[40, 40, 600]`, начало эффекта на `sampled_z + 300`; при нулевом локальном
|
||||
смещении концы quad находятся на `sampled_z` и `sampled_z + 600`. Opcode 3
|
||||
задаёт материал, локальное смещение, scale и lifetime visual quad. Opcode 1
|
||||
вычисляется отдельно как динамический point light и не задаёт размеры или UV
|
||||
quad.
|
||||
CPU не подменяет numeric поля шириной или линейным lifetime fade.
|
||||
|
||||
Звуки возвращаются рядом с графикой как `SoundEvent`:
|
||||
|
||||
```text
|
||||
StartLoop -> начать дождевой loop
|
||||
SetLoopVolume -> обновить его громкость при интерполяции погоды
|
||||
StopLoop -> остановить loop
|
||||
OneShot -> воспроизвести разовый звук молнии
|
||||
```
|
||||
|
||||
Каждое событие содержит архив, имя, мировую позицию и линейную громкость.
|
||||
Для FX-звука также передаются native `min_distance`, `max_distance` и
|
||||
`frequency_ratio`; у дождевого loop используются `0`, бесконечность и `1`.
|
||||
Событие `OneShot` молнии использует ту же случайно выбранную позицию, что и
|
||||
визуальный эффект, а его диапазон и частота читаются из opcode 2: в
|
||||
поставленном `env_lightning` это `100`, `1500` и `1`. Audio backend загружает
|
||||
объявленный архив и запись лениво, кэширует проверенный sample, создаёт
|
||||
отдельный spatial source, применяет DirectSound range/pan и текущий listener.
|
||||
Отсутствие устройства вывода не должно останавливать симуляцию.
|
||||
|
||||
## Что остаётся за владельцем
|
||||
|
||||
`EnvironmentSystem` не извлекает камеру из renderer, не разрешает материалы
|
||||
или звуки и не выполняет финальный GPU pass. Владелец должен передать основу
|
||||
камеры, projection, реальный viewport, `SkyMaterials` и декодированный FXID.
|
||||
Из projection и viewport модуль сам получает native scalar осадков.
|
||||
|
||||
CPU-контракт проверяет движение, wrap, количество, время молнии и порядок
|
||||
звуковых переходов. Он не заявляет совместимость с нераскрытой семантикой
|
||||
всех FX opcode, с точным native RNG sequence или с конкретным mixer/device
|
||||
backend. Подробное описание связи с listener и mission audio находится в
|
||||
[томе VI](../tomes/06-behavior.md#погода-осадки-и-звуковые-события).
|
||||
@@ -38,13 +38,9 @@ MAT0 имеет type ID `0x3054414D`, обычно расположен в `Mate
|
||||
|
||||
```c
|
||||
#pragma pack(push, 1)
|
||||
struct Mat0PrefixV4Plus {
|
||||
struct Mat0Header {
|
||||
uint16_t phase_count;
|
||||
uint16_t animation_block_count;
|
||||
uint8_t metadata_a;
|
||||
uint8_t metadata_b;
|
||||
uint32_t metadata_c_raw;
|
||||
uint32_t metadata_d_raw;
|
||||
};
|
||||
|
||||
struct Phase34 {
|
||||
@@ -54,8 +50,78 @@ struct Phase34 {
|
||||
#pragma pack(pop)
|
||||
```
|
||||
|
||||
Versioned fields читаются только если версия их содержит. Для старых версий
|
||||
используются runtime defaults, а raw values сохраняются.
|
||||
`attr1` читается из NRes entry metadata и сохраняется вместе с разрешённым
|
||||
материалом. Это runtime flags материала; его нельзя путать с `attr2`, который
|
||||
задаёт версию payload. Versioned prefix bytes читаются только если версия их
|
||||
содержит, а raw values сохраняются для последующих слоёв.
|
||||
|
||||
### Pipeline category
|
||||
|
||||
Call site извлекает native category как `(attr1 >> 2) & 0xF` и передаёт её в
|
||||
`LegacyPipelineState::with_material_category`. Таблица содержит пять
|
||||
подтверждённых категорий; helper возвращает `None` для остальных значений и не
|
||||
меняет уже выбранные depth и cull state.
|
||||
|
||||
| category | source/destination colour factors | alpha test |
|
||||
| ---: | --- | --- |
|
||||
| 0 | `ONE / ZERO`, blending disabled | disabled |
|
||||
| 1 | `SRC_ALPHA / INV_SRC_ALPHA` | enabled, `GREATER_EQUAL` |
|
||||
| 2 | `SRC_ALPHA / ONE` | enabled, `GREATER_EQUAL` |
|
||||
| 3 | `ZERO / SRC_COLOR` | enabled, `GREATER_EQUAL` |
|
||||
| 4 | `DEST_COLOR / SRC_COLOR` | enabled, `GREATER_EQUAL` |
|
||||
|
||||
The mapping comes from the native five render-state pairs selected by the
|
||||
material category. Alpha reference remains draw-range data, while the Vulkan
|
||||
pipeline key includes the blend mode, depth mode, cull mode and alpha-test
|
||||
variant so ranges with different native state do not share a pipeline.
|
||||
|
||||
Каждая phase занимает 34 байта: 18 parameter bytes и 16-byte C string имени
|
||||
текстуры. Loader переводит параметры в коэффициенты так:
|
||||
|
||||
| bytes | runtime value |
|
||||
| --- | --- |
|
||||
| `p0..p2` | additive RGB, `p / 255` |
|
||||
| `p3` | opacity, `p * 0.01` |
|
||||
| `p4..p7` | directional values, `p / 255` |
|
||||
| `p8..p11` | specular values, `p / 255` |
|
||||
| `p12..p15` | extra values, `p / 255` |
|
||||
| `p16` | integer power |
|
||||
| `p17` | signed TEXM page index |
|
||||
|
||||
Texture name и page index выбираются из текущей phase. Они не смешиваются с
|
||||
соседней phase.
|
||||
|
||||
## Animation
|
||||
|
||||
После phase table идут `animation_block_count` плотных блоков. Каждый блок имеет
|
||||
`u32 header_raw`, `u16 key_count`, затем `key_count` записей по три `u16`:
|
||||
|
||||
```text
|
||||
u32 header_raw
|
||||
u16 key_count
|
||||
repeat key_count:
|
||||
u16 phase_index
|
||||
u16 end_time_ms
|
||||
u16 raw_k2
|
||||
```
|
||||
|
||||
`header_raw & 7` задаёт режим: `0` loop, `1` ping-pong, `2` clamp, `3`
|
||||
random-per-query. Значения `4..7` сохраняются как unknown. `header_raw >> 3`
|
||||
является interpolation mask: bits `1`, `2`, `4`, `8` смешивают группы
|
||||
`p0..p2`, `p4..p6`, `p8..p10`, `p12..p14`, а bit `16` смешивает `p3`.
|
||||
Компоненты `p7`, `p11`, `p15`, `p16` и `p17` копируются из текущей phase.
|
||||
|
||||
Время для блока вычисляется как `clock_ms - wear_row_start_ms` с wrapping
|
||||
`u32` subtraction. Для loop берётся остаток от последнего `end_time_ms`, для
|
||||
ping-pong нечётный цикл идёт в обратном направлении, clamp после duration
|
||||
выбирает последнюю phase, а random-per-query использует переданное случайное
|
||||
значение по модулю duration. Ключ выбирается по интервалу
|
||||
`previous_end <= local_time < end_time`; следующий ключ циклически замыкается на
|
||||
первый. На ping-pong turnaround и clamp после duration native routine проходит
|
||||
общий float interpolation path, включая unsigned subtraction endpoint arithmetic;
|
||||
это может дать extrapolated coefficients при ненулевой mask. Переданный в
|
||||
sampler `animation_block_index` уже выбран вызывающим runtime из block table;
|
||||
packed WEAR handle и номер строки WEAR являются отдельными значениями.
|
||||
|
||||
## Fallback
|
||||
|
||||
|
||||
+79
-21
@@ -19,7 +19,7 @@ type 10 strings and node names
|
||||
type 13 Batch20 records
|
||||
type 15 auxiliary stream
|
||||
type 17 auxiliary data
|
||||
type 18 rare stream
|
||||
type 18 packed UV1 (optional secondary coordinates)
|
||||
type 19 animation frame map
|
||||
type 20 rare auxiliary table
|
||||
```
|
||||
@@ -40,23 +40,77 @@ struct Node38 {
|
||||
};
|
||||
```
|
||||
|
||||
`slot_index[lod * 5 + group]` выбирает geometry slot. `0xFFFF` означает
|
||||
отсутствие геометрии для комбинации LOD/group.
|
||||
В исходном layout это 3 состояния по 5 LOD: `slot_index[state * 5 + lod]`.
|
||||
Публичные Rust-обёртки пока сохраняют исторические имена `Lod` и `Group`,
|
||||
поэтому в `selected_slot` аргумент `Lod` задаёт native state (0..2), а
|
||||
`Group` задаёт native LOD (0..4). `0xFFFF` означает отсутствие геометрии для
|
||||
комбинации state/LOD.
|
||||
|
||||
Validated `ModelAsset` также сохраняет decoded type 8 keys и type 19 map как
|
||||
Validated `ModelAsset` сохраняет decoded type 8 keys и type 19 map как
|
||||
`ModelAnimation`. `node38_fallback_pose` возвращает pose по `fallback_key`,
|
||||
то есть доказанный static input. `parent_or_link == 0xFFFF` означает root;
|
||||
иначе это parent index, обязательно меньший индекса child. Этот контракт
|
||||
подтверждён на тестах анимации с оригинальными ресурсами обеих частей и защищён fallback-ом:
|
||||
модель с нарушенным порядком не получает придуманную hierarchy.
|
||||
а `node38_fallback_hierarchy` собирает parent-before-child hierarchy.
|
||||
`node38_sampled_hierarchy` использует type 19 map в пределах объявленного
|
||||
числа кадров и интерполирует соседние type 8 keys; при отсутствии usable map
|
||||
или выходе за frame count используется fallback key. Для покомпонентного
|
||||
sampling `node38_sampled_hierarchy_at_times` принимает отдельный optional float
|
||||
time для каждого узла: `Some(time)` выбирает type 19 frame по округлению
|
||||
`time - 0.5` к ближайшему целому с ties-to-even, затем интерполирует выбранный
|
||||
type 8 key и его следующий key в исходный float time; `None` использует
|
||||
fallback key. Значения должны быть конечными и неотрицательными. Это
|
||||
portable-поведение при default-nearest rounding; полного совпадения со всеми
|
||||
режимами округления x87 оно не заявляет. `parent_or_link == 0xFFFF`
|
||||
означает root, иначе это parent index, который должен быть меньше индекса
|
||||
child. Модель с нарушенным порядком не получает придуманную hierarchy.
|
||||
|
||||
В legacy-camera static preview стандартный узел уже получает свой fallback pose
|
||||
до внешнего TMA/Iron3D transform. Parent pose поворачивает child translation,
|
||||
затем translation суммируется, а rotations умножаются; после полученной global
|
||||
pose применяется `Rz * Ry * Rx`, scale и mission translation. Геометрия
|
||||
намеренно дублируется на draw-range узла, потому что один source vertex может
|
||||
быть нарисован разными node poses. Это static fallback hierarchy, а не полная
|
||||
animation parity: dynamic type-19 frame-map sampling остаётся отдельной задачей.
|
||||
Type 8 записывает quaternion в порядке WXYZ, после decode `AnimKey24` хранит
|
||||
его как XYZW. Native AniMesh `+0x12560` строит local matrix через NGI32
|
||||
`g_FastProc + 0x38`; scalar implementation выдаёт transpose обычной active
|
||||
`Pose` basis (`M01=2(xy+zw)`, `M02=2(xz-yw)`, `M10=2(xy-zw)`). Поэтому путь
|
||||
Node38 MSH сопрягает source-local quaternion ровно один раз после sampling и
|
||||
до hierarchy composition (меняет знаки компонент `x`, `y`, `z`). Для fallback
|
||||
poses применяется та же граница. Общий animation decoder и другие форматы
|
||||
анимации не изменяются.
|
||||
|
||||
В legacy-camera static preview без `--static-animation-frame <u16>` используются
|
||||
сохранённые начальные значения из явной ссылки каждого prototype на `.ctl`.
|
||||
Узлы без control binding получают time `0`. Для каждой component instance
|
||||
обрабатываются её CTLD bindings, поэтому общий deduplicated MSH не смешивает
|
||||
controls разных компонентов. Строки идут в source order, и последняя включённая
|
||||
строка для одного узла заменяет предыдущую. Flag `0x01` выполняет один wrap
|
||||
значения blend на единицу, иначе значение ограничивается `[0, 1]`; flag `0x02`
|
||||
инвертирует результат (`1 - blend`), а строки с `0x04` пропускаются. Выбранный
|
||||
float time равен `(1 - blend) * frame_a + blend * frame_b`. Если он выходит за
|
||||
type 19 map, MSH sampler использует fallback key; если после одной wrap/clamp и
|
||||
invert получается отрицательное или нечисловое время, preview завершится
|
||||
понятной ошибкой. Явный `--static-animation-frame` остаётся global override
|
||||
для всех компонентов. Parent pose поворачивает child translation, затем
|
||||
translation суммируется, а rotations умножаются; после полученной global pose
|
||||
применяется `Rz * Ry * Rx`, scale и mission translation. Геометрия намеренно
|
||||
дублируется на draw-range узла, потому что один source vertex может быть
|
||||
нарисован разными node poses. Это только начальная статическая pose: она не
|
||||
запускает игровой Control/AI и не обещает полного совпадения с runtime или
|
||||
всеми x87 rounding modes.
|
||||
|
||||
## Vertex streams
|
||||
|
||||
Основные vertex streams имеют фиксированный source index. У type 3 `attr1`
|
||||
задаёт количество вершин, а `attr3` — размер записи в байтах. Type 4
|
||||
содержит четыре signed bytes нормали. Type 5 и optional type 18 содержат по
|
||||
два little-endian `uint16` на вершину:
|
||||
|
||||
```c
|
||||
struct PackedUv16x2 {
|
||||
uint16_t u;
|
||||
uint16_t v;
|
||||
};
|
||||
```
|
||||
|
||||
Type 5 — primary UV0, type 18 — secondary UV1 для lightmap/detail stage.
|
||||
Для type 18 loader требует `attr3 == 4`, совпадение `attr1` и длины payload,
|
||||
а также одинаковое число записей с type 3. Обе пары декодируются как
|
||||
`packed / 1024.0`; renderer обращается к UV1 по тому же `source_index`, что и
|
||||
к UV0. Отсутствующий type 18 означает, что secondary stage не имеет authored
|
||||
координат.
|
||||
|
||||
## Slot and batch
|
||||
|
||||
@@ -82,16 +136,20 @@ Type 13 задаёт draw ranges:
|
||||
#pragma pack(push, 1)
|
||||
struct Batch20 {
|
||||
uint16_t batch_flags;
|
||||
uint16_t material_index;
|
||||
uint16_t opaque4;
|
||||
uint16_t opaque6;
|
||||
uint16_t material_index_hi; // +0x02, native high selector/slot field
|
||||
uint8_t material_index; // +0x04, WEAR material selector
|
||||
uint8_t lightmap_index; // +0x05, 0xFF means no lightmap
|
||||
uint16_t local_batch_index; // +0x06
|
||||
uint16_t index_count;
|
||||
uint32_t index_start;
|
||||
uint16_t opaque14;
|
||||
uint16_t vertex_count; // +0x0E
|
||||
uint32_t base_vertex;
|
||||
};
|
||||
#pragma pack(pop)
|
||||
```
|
||||
|
||||
Index check выполняется как `base_vertex + index < vertex_count` для всего
|
||||
используемого slice.
|
||||
Byte `material_index` выбирает строку WEAR; high word at `+0x02` сохраняется
|
||||
отдельно для native overrides. Loader проверяет, что
|
||||
`index_start + index_count` остаётся внутри type 6, а каждый использованный
|
||||
индекс через `base_vertex` попадает в position stream. `vertex_count` и
|
||||
`local_batch_index` доступны renderer для исходного draw contract.
|
||||
|
||||
@@ -69,6 +69,14 @@ projection matrix contract.
|
||||
|
||||
## Parity risks
|
||||
|
||||
`VulkanStaticCamera::from_legacy_d3d7` keeps the D3D7 view and projection
|
||||
reconstruction in `fparkan-render`, then negates the vertical projection term
|
||||
once at the Vulkan adapter boundary. D3D7's top-down viewport places positive
|
||||
NDC Y at the top; the Vulkan swapchain uses a positive viewport height, so the
|
||||
adapter flips NDC Y while the shader preserves `gl_Position`. The generic
|
||||
`from_row_major_view_projection` path remains unchanged; its callers supply
|
||||
Vulkan-ready matrices, as the free-flight camera already does.
|
||||
|
||||
- x87 precision and rounding;
|
||||
- scalar/SIMD `g_FastProc` differences;
|
||||
- object, batch and transparent primitive order;
|
||||
|
||||
+55
-19
@@ -359,9 +359,16 @@ local.set_translation(pose.position);
|
||||
world[n] = world[parent(n)] * local;
|
||||
```
|
||||
|
||||
Для parity особенно важны x87-compatible округление при выборе frame index и
|
||||
порядок операций. Одинаковая формула на SSE может выбрать соседний кадр возле
|
||||
границы.
|
||||
Статический Vulkan preview берёт начальный кадр узла из явной ссылки прототипа
|
||||
на CTLD `.ctl`. Используются только включённые строки, а при повторе node ID
|
||||
поздняя строка заменяет раннюю. Без `--static-animation-frame` preview применяет
|
||||
эти настройки узлов; узлы без настройки остаются на кадре `0`. Явный
|
||||
`--static-animation-frame N` переопределяет их и задаёт `N` всем узлам.
|
||||
Результат — одна статическая поза, не обновление анимации, Control или AI.
|
||||
MSH sampler выбирает type 19 frame округлением `(time - 0.5)` к ближайшему
|
||||
целому с ties-to-even, затем интерполирует выбранный type 8 key и следующий
|
||||
key в исходном `time`; это portable-вариант native x87 conversion в default
|
||||
rounding mode, без обещания совпадения при любом x87 control word.
|
||||
|
||||
Проверки animation data:
|
||||
|
||||
@@ -611,11 +618,20 @@ Lightmap не является обычной diffuse texture. WEAR содерж
|
||||
diffuse texture ломает LOD, atlas coordinates и динамическую модуляцию.
|
||||
|
||||
Тени проходят отдельным render pass. Terrain содержит пути для теней зданий и
|
||||
роботов, ограничения максимального числа, detail level и smoothing. Доказаны
|
||||
shadow manager/pass, настройки detail/smoothing/count и зависимость от
|
||||
Terrain/CShade; полная формула projection geometry для каждого caster требует
|
||||
dynamic trace. Unknown settings из `shade.cfg` читаются и сохраняются по
|
||||
именам, а не заменяются произвольными modern defaults.
|
||||
роботов, ограничения максимального числа, detail level и smoothing. Реализованы
|
||||
native visibility по шести Vulkan clip-плоскостям с радиусом sphere +100,
|
||||
порядок кандидатов и лимит 20 до построения страниц, projected-size LOD,
|
||||
directional/point light contribution, native page layout, raster, smoothing и
|
||||
receiver projection с edge clipping. Unknown settings из `shade.cfg` читаются и
|
||||
сохраняются по именам, а не заменяются произвольными modern defaults. Для native ray query
|
||||
surface descriptor в `+0x10` требует наличия всех переданных bits, а descriptor
|
||||
в `+0x14` исключает поверхности с любым переданным bit. Общий shadow query
|
||||
использует full exclusion mask `0x2000`; actor four-ray path получает compact
|
||||
mask `0x8008`, который landscape boundary разворачивает в full mask
|
||||
`0x00200020`. Sun path передаёт full `0x20`, кодирует его как compact bit `8`
|
||||
и затем получает тот же full bit после разворачивания. Hit record хранит в
|
||||
`+0x20` квадрат евклидовой длины луча, поэтому native four-ray fade использует
|
||||
`distance² * 1e-4`, а не линейную дальность.
|
||||
|
||||
Atmosphere manager создаёт world objects для фоновых и погодных явлений.
|
||||
Отдельно подтверждены lightning, sun render, flare, `env_lightning`, rain
|
||||
@@ -624,6 +640,9 @@ background sound и обязательные ссылки на lightning effect.
|
||||
требует screen position и occlusion test, rain -- области рядом с observer,
|
||||
sound -- listener. Их нельзя один раз запечь в terrain.
|
||||
|
||||
Sun occlusion начинается от точки `camera + 0.5 * normalize(sun - camera)`;
|
||||
этот offset является частью native query, а не произвольным bias renderer-а.
|
||||
|
||||
RNG для lightning, atmosphere phases и FX должен иметь стабильный порядок.
|
||||
Даже правильный средний интервал не даёт повторяемый кадр, если random values
|
||||
запрашиваются в другой последовательности.
|
||||
@@ -677,11 +696,14 @@ settings ID сохраняются.
|
||||
|
||||
```text
|
||||
opcode = command_word & 0xFF
|
||||
enabled = (command_word >> 8) & 1
|
||||
native_flag = (command_word >> 8) & 1
|
||||
```
|
||||
|
||||
Bits 9-31 являются частью данных и сохраняются. Между командами нет
|
||||
выравнивания. Размер команды, включая word:
|
||||
`native_flag` передаётся созданному native command object как режим/feature
|
||||
flag и не отключает команду. Например, реальный `env_lightning` использует
|
||||
`native_flag == 0` для opcode 3, opcode 1 и opcode 2, и все три команды
|
||||
исполняются. Bits 9-31 являются частью данных и сохраняются. Между командами
|
||||
нет выравнивания. Размер команды, включая word:
|
||||
|
||||
```text
|
||||
opcode 1 224 байта
|
||||
@@ -896,8 +918,11 @@ Shade cache в GOG `Terrain.dll` имеет vtable RVA `0x643D0`. Его таб
|
||||
находится по `+320`; банки начинаются с `+332`, их шаг — 212 байт. Lookup
|
||||
RVA `0x10910` возвращает временное представление по `+24`, а не саму запись.
|
||||
Построение в RVA `0x10280` и `0x12e20` использует созданную загрузчиком геометрию.
|
||||
Поэтому повторить terrain shader только разбором WEAR нельзя. Точная композиция
|
||||
слоёв и микротекстур остаётся открытым вопросом.
|
||||
В рабочем Vulkan-пути `TerrainMaterialLayers` разрешает для каждого terrain slot
|
||||
базовую, detail, overlay и overlay-detail фазы из WEAR/MAT0; их UV, alpha и
|
||||
lightmap state передаются в `VulkanStaticMaterial`, а shader последовательно
|
||||
сэмплирует эти четыре стадии. Animation clock обновляет выбранные фазы и
|
||||
коэффициенты без пересборки terrain mesh, сохраняя native type-14 overlay mask.
|
||||
|
||||
## Реализация Vulkan
|
||||
|
||||
@@ -905,12 +930,23 @@ RVA `0x10910` возвращает временное представление
|
||||
получается ключ Vulkan pipeline. Alpha reference передаётся отдельно через
|
||||
push constant; он не требует нового pipeline.
|
||||
|
||||
Статический путь `fparkan-game` загружает ландшафт и MSH-компоненты выбранных
|
||||
объектов. Локальные material slots разрешаются через WEAR и MAT0 перед
|
||||
объединением геометрии. Индексы GPU имеют тип `u32`: вся карта может содержать
|
||||
больше 65 535 вершин, даже если каждый исходный mesh использует `u16`.
|
||||
Текущий путь берёт базовую текстуру Land2; свет, составные слои и атмосфера
|
||||
пока не участвуют в этом статическом изображении.
|
||||
Рабочий путь `fparkan-game` загружает terrain и все выбранные mission roots
|
||||
(по умолчанию весь список объектов), разрешая локальные material slots через
|
||||
WEAR и MAT0 до объединения геометрии. Индексы GPU имеют тип `u32`: вся карта
|
||||
может содержать больше 65 535 вершин, даже если каждый исходный mesh использует
|
||||
`u16`. Terrain передаёт в shader базовый, detail и overlay stages, lightmap и
|
||||
animation phase; object batches сохраняют их legacy blend/depth/alpha state.
|
||||
|
||||
После загрузки world path создаёт интерактивную free-flight камеру (`WASD`,
|
||||
`E/Q`, `Shift`, `Ctrl`, RMB relative-look), обновляет listener и на каждом
|
||||
кадре собирает environment frame. Mission-local `sky.ske`/`sky.wea` выбирают
|
||||
атмосферное расписание и sky materials, `effects.rlb/env_lightning` даёт
|
||||
lightning visual; weather particles, flares, sun/moon, point lights,
|
||||
projected shadows и audio events обновляются вместе с камерой и временем.
|
||||
`--atmosphere-seconds` задаёт старт времени, `--frames 0` оставляет цикл
|
||||
бесконечным, а `--preview-roots` служит только диагностическим ограничителем.
|
||||
`--legacy-camera-capture` выбирает воспроизводимую камеру без free-flight
|
||||
управления.
|
||||
|
||||
Для сохранения кадра surface должен поддерживать `TRANSFER_SRC`. Renderer
|
||||
копирует последний отправленный swapchain image в host-visible buffer и
|
||||
|
||||
+169
-34
@@ -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 идут пять `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`). Остальные поля
|
||||
нельзя выводить из этой позовой привязки.
|
||||
|
||||
CTLD является составным count-driven форматом, а не фиксированной struct.
|
||||
Parser должен:
|
||||
Пример `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` подтверждают границы этих
|
||||
сравнений.
|
||||
|
||||
- прочитать prefix и все счётчики с проверкой переполнения;
|
||||
- вычислить границы секций по их counts;
|
||||
- сохранять неизвестные records в typed raw containers;
|
||||
- требовать точного завершения payload;
|
||||
- не использовать размер одного популярного варианта как универсальный layout.
|
||||
|
||||
Полная предметная семантика всех секций ещё не доказана, но существующие файлы
|
||||
можно безопасно читать, индексировать и сохранять.
|
||||
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
|
||||
|
||||
@@ -28,8 +28,9 @@ FParkan развивается небольшими законченными и
|
||||
|
||||
Оригинальные ресурсы остаются в установленной игре. Локальные тесты с ними
|
||||
помечены `#[ignore]`, чтобы обычная проверка работала без коммерческих файлов.
|
||||
Сообщение об ошибке должно назвать ресурс и причину: например, какой материал
|
||||
сослался на отсутствующую текстуру. Отдельный отчёт для каждого запуска не нужен.
|
||||
Сообщение об ошибке должно сразу назвать ресурс и причину: например, какой
|
||||
материал сослался на отсутствующую текстуру. Поэтому диагностика остаётся
|
||||
частью самого запуска и не зависит от отдельного отчёта.
|
||||
|
||||
## Числа и порядок вычислений
|
||||
|
||||
|
||||
+18
-77
@@ -241,45 +241,20 @@ near/far mapping, handedness или initial camera selection. Важно, что
|
||||
`ICamera::GetTransformMatrix` (RVA `0x4F850`) ведут только в obsolete-call
|
||||
stubs и не дают usable ABI.
|
||||
|
||||
Live elevated read-only probe впервые подтвердил relocation-aware runtime связь:
|
||||
`Terrain.dll` был загружен по `0x02510000`, global `base + 0x7355C` содержал
|
||||
non-null `0x0B37DF08`, а первый dword этого объекта был `0x025765B4` — ровно
|
||||
relocated `off_100665B4` из `LoadCamera` construction path. Это доказывает
|
||||
live camera object с outer vtable, но одновременно исправляет прежнее слишком
|
||||
сильное сопоставление offsets: raw read `global + 0x10` не дал finite 4x4 matrix,
|
||||
а `+0x234` был zero в данном sample. Receiver static procedures `0x4D740`/
|
||||
`0x4D9C0` и exported global ещё не доказаны как один layout без interface
|
||||
adjustment; их offsets остаются unassigned до recovery selector relationship.
|
||||
Глобальная camera boundary имеет более точное статическое описание.
|
||||
`stdGetCurrentCamera2` — короткий getter Terrain по RVA `0x4FD80`, который
|
||||
читает указатель из global RVA `0x7355C`; initializer по RVA `0x4D4D0`
|
||||
запрашивает selector `8` у своего `this` и сохраняет полученный interface
|
||||
pointer. Запрос selector `18` у landscape object проходит через RVA `0x106D0`
|
||||
и `0x107E0`, возвращает subobject по `base + 0x138`.
|
||||
|
||||
Локальная IDA-база уточняет адреса этой связи: `stdGetCurrentCamera2` — это
|
||||
шестибайтный getter по RVA `0x4FD80`, который возвращает `dword` по RVA
|
||||
`0x7355C`. Единственный найденный **direct static** initializer этого global — функция Terrain
|
||||
по RVA `0x4D4D0`: она запрашивает selector `8` у своего `this` и сохраняет
|
||||
полученный interface pointer. Это доказывает адрес хранения и путь заполнения,
|
||||
но не разрешает трактовать pointer как конкретный layout камеры либо читать его
|
||||
как runtime evidence без доступа к процессу на том же уровне привилегий.
|
||||
|
||||
Elevated live sampling теперь доказывает, что direct static xref не исчерпывает
|
||||
runtime writers: за 25 секунд autoplay global переключился между тремя heap
|
||||
pointer, все с relocated outer vtables `0x025765B4`/`0x02576558`. У двух объектов
|
||||
paired blocks `+0x2C/+0x3C/+0x4C` и `+0x6C/+0x7C/+0x8C` синхронно несли
|
||||
world-like translation, например `(491.562, 761.551, 7.361)`; третий давал
|
||||
normalized-looking `(0.098, 0.018, 0.856)` и не совпадал с paired block.
|
||||
Наблюдение согласуется с автоматическими camera switches, но не маркирует mode;
|
||||
оно запрещает называть `0x4D4D0` единственным runtime writer и требует recovery
|
||||
indirect/unanalyzed write path.
|
||||
|
||||
Outer vtable `off_100665B4` теперь даёт exact transform adjustment для
|
||||
world-like sample. Slot `+0x54` вызывает у subobject `outer + 4` slot `+0x20`
|
||||
с selector `0`, затем копирует returned `+0x0C/+0x1C/+0x2C`. В live объекте
|
||||
selector field `outer + 0x10` был `0xFFFFFFFF`; реализация selector `0` при
|
||||
этом возвращает `outer + 0x20`. Значит observed triple
|
||||
`outer + 0x2C/+0x3C/+0x4C` — доказанная translation часть active affine
|
||||
transform, а не корреляция. Selector `2` возвращает paired block `outer + 0x60`;
|
||||
outer slot `+0x70` применяет `atan2` к его axis values, что доказывает
|
||||
orientation-angle path. Названия полей, angle order и handedness пока не
|
||||
установлены, но raw affine transform и translation можно сохранять как
|
||||
backend-neutral camera pose без догадок.
|
||||
Внешняя camera vtable по RVA `0x665B4` имеет slot `+0x54`, который вызывает у
|
||||
subobject `outer + 0x4` slot `+0x20` с selector `0` и копирует возвращённые
|
||||
компоненты `+0x0C`, `+0x1C` и `+0x2C`. При значении selector field
|
||||
`outer + 0x10 = -1` selector `0` возвращает `outer + 0x20`; selector `2`
|
||||
возвращает `outer + 0x60`, а slot `+0x70` выводит углы через `atan2`.
|
||||
Это фиксирует пути чтения положения и ориентации, но не назначает имена полям,
|
||||
порядок углов или handedness.
|
||||
|
||||
### Vtable и interface negotiation
|
||||
|
||||
@@ -288,6 +263,11 @@ backend-neutral camera pose без догадок.
|
||||
world traversal; camera и viewport получаются через selector-based interface
|
||||
calls; shared objects используют ранний slot как AddRef-подобную операцию.
|
||||
|
||||
Запрос selector `18` у landscape object проходит через RVA `0x106D0` и
|
||||
`0x107E0`, возвращает subobject по `base + 0x138`, а его vtable slot `+0x18`
|
||||
ведёт через RVA `0x14230` к обработчику `0x127D0`. Это geometry-interface
|
||||
boundary, отдельная от хранения активной camera.
|
||||
|
||||
Правила реконструкции:
|
||||
|
||||
1. Зафиксировать byte offset slot и число аргументов.
|
||||
@@ -299,45 +279,6 @@ calls; shared objects используют ранний slot как AddRef-по
|
||||
Нельзя добавлять virtual destructor в начало reconstructed interface: это
|
||||
сдвинет все slots.
|
||||
|
||||
### ABI-матрица Частей 1 и 2
|
||||
|
||||
Во всех пятнадцати DLL совпадают export names, ordinals и import sets. Общее
|
||||
число exports остаётся 313. Обе полные части содержат 1 134 imported function
|
||||
slots; значение 1 126 относится к демоверсии и хранится отдельно.
|
||||
|
||||
Побайтно идентичны девять DLL:
|
||||
|
||||
```text
|
||||
ai.dll
|
||||
Behavior.dll
|
||||
Joystick.dll
|
||||
MisLoad.dll
|
||||
Net.dll
|
||||
Ngi32.dll
|
||||
Terrain.dll
|
||||
Wizard.dll
|
||||
World3D.dll
|
||||
```
|
||||
|
||||
Пересобраны `AniMesh.dll`, `ArealMap.dll`, `Control.dll`, `Effect.dll`,
|
||||
`iron3d.dll`, `services.dll`.
|
||||
|
||||
Изменение export RVA:
|
||||
|
||||
```text
|
||||
AniMesh 2 / 2
|
||||
Control 5 / 5
|
||||
iron3d 8 / 8
|
||||
services 6 / 6
|
||||
ArealMap 0 / 9
|
||||
Effect 0 / 2
|
||||
```
|
||||
|
||||
Нулевое изменение export RVA не доказывает идентичность тела функции:
|
||||
`ArealMap.dll` и `Effect.dll` имеют изменённый `.text` при прежних адресах
|
||||
exports. Compatibility headers фиксируют внешний ABI один раз, но внутренняя
|
||||
таблица адресов, тестов и semantic deltas выбирается по build fingerprint.
|
||||
|
||||
## Файловая поверхность
|
||||
|
||||
### Каталог как внешний API
|
||||
|
||||
Reference in New Issue
Block a user