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:
+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