# Runtime-модули
> Перевод [исходной страницы на английском языке](https://github.com/OmsiModsBR/OmsiLaunch/blob/63ccaf8cfc6c6d24a829d5e7a2a36ecc795e2744/docs/reference/runtime-modules.md) для OmsiLaunch 0.1.0-beta4. Нормативной является английская страница: при расхождениях приоритет имеют английская страница и код.
Runtime-модуль добавляет поведение в управляемый сеанс OMSI: функцию, которая работает вместе с сеансом (например, HUD карты или оптимизации загрузки), не становясь частью самого OmsiLaunch. Мост загружает модуль, предоставляет ему только то, что модуль объявил, и освобождает всё, чем модуль владеет, когда сеанс завершается или модуль даёт сбой. Эта страница — нормативный справочник по runtime-модулям в том виде, в каком они реализованы в `src/OmsiLaunch.Runtime.Modules` (манифест, параметры, допуск, хост жизненного цикла), в планировщике, в сеансовых программах контроллера (`ModuleSessionPrograms`, `ModuleSessionProcess`), в среде модулей плагина и во флагах модулей [CLI](https://omsilaunch.omsimods.com.br/ru/docs/reference/cli/index.md).
Стабильность: `EXPERIMENTAL`. Протокол моста 1 проверен в runtime в раунде B (управляемые модули, активация по содержимому, операции, `disable-module` и `fail-session`; нативные компоненты с IAT-хуками; см. [состояние проверки в runtime](https://omsilaunch.omsimods.com.br/ru/docs/status/runtime-validation-status/index.md)). Протокол моста 2 (параметры, активация по умолчанию, файл параметров, сеансовые программы, конфликты плагинов, уведомления, `get_value`) проверен только офлайн-тестами. Офлайн-гейты: гейты `module.*` из `tests/OmsiLaunch.ModuleTests` (манифест, целостность, выбор и активация, параметры, файл параметров, конфликты плагинов, сеансовые программы, запускаемые через свой job, сбой, внесённый на каждой фазе при каждой политике сбоев, жизненный цикл нативного компонента, очистка, handoff), `plugin-runtime.modules-lifecycle` (настоящий управляемый модуль, загруженный средой плагина), `runtime-modules.native-component` (настоящий нативный компонент, перехватывающий импорт и читающий значения через мост), `runtime-modules.plugin-conflict` и `native-hooks.self-check` (движок хуков).
Загрузка открыта: можно загрузить любой модуль, установленный ниже `plugins\OmsiLaunch.Modules`, кто бы его ни создал. OmsiLaunch не ручается за то, что модуль делает внутри OMSI. Гарантируется [граница безопасности](#security-boundary): модули никогда не выполняются внутри процесса OmsiLaunch, и ничто из предлагаемого OmsiLaunch не позволяет модулю выйти за пределы собственных прав OMSI.
## Где находятся модули
Каждый модуль — это каталог ниже каталога постоянного плагина:
```text
\plugins\OmsiLaunch.Modules\\module.json
\plugins\OmsiLaunch.Modules\\.dll
\plugins\OmsiLaunch.Modules\\
```
Имя каталога должно совпадать с `id` модуля. Модули устанавливает тот, кто их распространяет; сеанс никогда не записывает, не снимает snapshot и не восстанавливает там файлы, а модули не изменяют файлы установки через OmsiLaunch (в манифесте нет способа объявить overlay установки). Записываемые данные модуля находятся в его [каталоге данных](#data-directory).
## Манифест (`module.json`)
Схема `omsilaunch.runtime-module/v1`. Неизвестные ключи отклоняются, поэтому манифест, написанный для более поздней схемы, отвергается, а не понимается частично. Все пути задаются относительно каталога модуля (пути привязанного содержимого — относительно корня OMSI), используют `\` или `/` и не могут содержать `..` или диск.
| Ключ | Обязателен | Значение |
| --- | --- | --- |
| `schema` | да | `omsilaunch.runtime-module/v1`. |
| `id` | да | Идентификатор модуля: строчные буквы, цифры и `-`, от 2 до 48 символов, начинается с буквы или цифры. Он же является именем каталога модуля и пространством имён его операций. |
| `version` | да | `major.minor.patch`, при желании с `-suffix`. |
| `name` | нет | Отображаемое имя (не более 128 символов). |
| `bridge_protocol` | да | `{ "min": n, "max": n }`: протоколы моста, с которыми работает модуль. Эта сборка обслуживает протоколы `1` и `2`; модуль принимается, если его диапазон включает один из них. |
| `activation` | нет | `requested` (по умолчанию): загружается, когда его запрашивает сеанс. `content`: загружается также, когда сеанс запускает привязанную карту (требуется `content_binding`). `default` (протокол 2): загружается в каждом сеансе, если не отключён (см. [выбор модулей](#choosing-modules-for-a-session)); не может иметь `content_binding`. |
| `required_capabilities` | нет | Возможности (capability), без которых модуль работать не может (см. [возможности](#capabilities)). |
| `optional_capabilities` | нет | Возможности, которые модуль использует, когда они доступны; он проверяет их через `IsCapabilityAvailable`. |
| `content_binding` | нет | `{ "map": "maps\\\\global.cfg", "files": [ { "path", "sha256" } ] }`: модуль работает только на этой карте и только при неизменных указанных файлах. |
| `assets` | нет | Файлы, которые читает модуль, каждый — `{ "path", "sha256" }`. |
| `components` | да | Ровно один компонент `managed` `{ "kind": "managed", "path": ".dll", "sha256", "entry": "" }`; при необходимости один компонент `native` (`.dll`), компоненты `auxiliary` (`.exe`) и, с протоколом 2, один компонент `host` (`.exe`, [сеансовая программа](#session-programs) модуля). |
| `failure_policy` | да | `fail-session`, `disable-module` или `degrade` (см. [сбои](#failures)). |
| `operations` | нет | Операции, которые модуль предоставляет, пока сеанс работает: `{ "id", "kind": "read"\|"write"\|"action", "arguments": [ { "name", "required" } ], "timeout_ms" }`. |
| `settings` | нет | Протокол 2. Параметры, которые принимает модуль (см. [параметры](#settings)). |
| `conflicts` | нет | Протокол 2. `{ "plugins": [ ".dll" ] }`: DLL плагинов OMSI, которые не должны работать рядом с модулем (см. [конфликты](#conflicts-with-omsi-plugins)). |
| `timeouts` | нет | Бюджеты в миллисекундах: `arm_ms`, `gameplay_ready_ms`, `stop_ms` (по умолчанию по 5000, не более 30000); с протоколом 2 также `prepare_ms` (по умолчанию 120000, не более 1800000) и `host_stop_ms` (по умолчанию 10000, не более 60000) для сеансовой программы. Операции по умолчанию получают 2000 ms. |
| `diagnostics` | нет | `{ "trace": true\|false }`. |
Манифест, использующий возможность протокола 2 (`settings`, `conflicts`, активацию `default`, компонент `host`, `prepare_ms` или `host_stop_ms`), должен объявлять `bridge_protocol.min` равным 2.
Каждый файл, названный в `components` и `assets`, закреплён своим SHA-256. `module.json` не может закрепить сам себя.
```json
{
"schema": "omsilaunch.runtime-module/v1",
"id": "example-preload",
"version": "1.0.0",
"bridge_protocol": { "min": 2, "max": 2 },
"activation": "default",
"required_capabilities": ["bridge.events", "bridge.hooks.iat"],
"components": [
{ "kind": "managed", "path": "Example.Preload.dll", "sha256": "<64 hex digits>", "entry": "Example.Preload.PreloadModule" },
{ "kind": "native", "path": "example-preload.dll", "sha256": "<64 hex digits>" },
{ "kind": "host", "path": "ExamplePreloader.exe", "sha256": "<64 hex digits>" }
],
"settings": [
{ "key": "ram_budget_mb", "type": "integer", "default": "auto", "auto": true, "min": 256, "max": 12288, "flag": "example-ram" },
{ "key": "monitor", "type": "boolean", "default": false, "flag": "example-monitor" }
],
"conflicts": { "plugins": ["OldExamplePlugin.dll"] },
"failure_policy": "degrade",
"timeouts": { "prepare_ms": 300000 }
}
```
## Целостность
Каждый закреплённый файл должен существовать, не должен быть доступен через junction или символическую ссылку и должен иметь закреплённый SHA-256; иначе модуль отклоняется с `OL_E_MODULE_INTEGRITY`. Так обнаруживается повреждённый или наполовину обновлённый модуль. Это не решение о доверии: модуль не сверяется ни со списком, ни с подписью, ни с издателем.
Проверка повторяется там, где стартует код модуля: плагин загружает модуль, только если его `module.json` по-прежнему имеет SHA-256, который оценил план, и все закрепления по-прежнему выполняются, а контроллер запускает сеансовую программу, только проверив закрепление этой программы.
## Граница безопасности
OmsiLaunch не контролирует, что модуль делает внутри OMSI: его управляемые и нативные компоненты работают в процессе OMSI с теми же правами, что и любой плагин OMSI. OmsiLaunch гарантирует следующее:
1. Процесс OmsiLaunch никогда не загружает код модулей. Код модуля выполняется внутри OMSI, во вспомогательной программе, запущенной изнутри OMSI, или в сеансовой программе модуля.
2. Программы, которые OmsiLaunch запускает для модулей, — это файлы собственного каталога модуля, проверенные по закреплению и запущенные с командной строкой, которую задаёт OmsiLaunch. Сеансовая программа выполняется в объекте job, который завершает её вместе с сеансом, не допускает второго процесса (она не может запускать другие программы) и отклоняет изменения системных параметров, настроек дисплея, рабочего стола, а также выход из системы и выключение. Она наследует только два дескриптора своих каналов (pipe).
3. Всё, что объявляет модуль, остаётся внутри его каталога, а записываемые данные — внутри `.omsilaunch\module-data\`.
4. Каналы OmsiLaunch (локальное управление, startup handoff, телеметрия, runtime-команды) остаются ограниченными текущим пользователем и привязанными к сеансу; ни один из них не позволяет вызывающей стороне запустить программу, читать или записывать файлы вне установки либо изменять системные параметры.
5. OmsiLaunch никогда не повышает права. Модуль работает с правами пользователя, запустившего сеанс.
6. Текст, поступающий от модуля (уведомления, события, значения), рассматривается как данные: ограничен по длине, без управляющих символов, никогда не выполняется, не открывается и не воспринимается как ссылка.
## Возможности
`required_capabilities` и `optional_capabilities` называют либо нативную возможность работающей сборки (столбец нативных возможностей [каталога возможностей](https://omsilaunch.omsimods.com.br/ru/docs/reference/capabilities/index.md#capability-catalog), например `time.read` или `d3d.texture`), либо функцию моста:
| Функция моста | Что получает модуль |
| --- | --- |
| `bridge.events` | `Emit`: runtime-события `module..`. |
| `bridge.background-threads` | `RunBackground`: отслеживаемые фоновые потоки. |
| `bridge.auxiliary-processes` | `StartAuxiliaryProcess`: вспомогательные процессы, привязанные к сеансу. |
| `bridge.hooks.iat` | Нативный компонент: хуки на импорты `Omsi.exe` (`iat:!`). |
| `bridge.hooks.d3d9` | Нативный компонент: хуки на методы живого устройства Direct3D 9 (`d3d9:device.`). Требует нативной возможности `d3d.texture`. |
Имя, не относящееся ни к тому, ни к другому, отклоняется при планировании (`OL_E_MODULE_CAPABILITY_UNRESOLVED`). В сборке OMSI 2 без известной раскладки нативная возможность, которую не разрешает исполняемый файл, определяется в процессе; если и там она не разрешена, модуль не загружается.
## Выбор модулей для сеанса
Модуль входит в сеанс одним из четырёх способов; какой именно, сообщает `PlannedModule.Activation`:
| Активация | Когда |
| --- | --- |
| `requested` | Его запрашивает сеанс: `LaunchSpec.Modules.Enable`, `/module:` или `enabled: true` в выбранном пресете профиля сеанса. |
| `settings-file` | `enabled=true` в его секции [файла параметров](#the-settings-file). |
| `content` | Его активация — `content`, и сеанс запускает его привязанную карту. |
| `default` | Его активация — `default`. |
Модуль не допускается в сеанс через `LaunchSpec.Modules.Disable`, `/no-module:`, `enabled: false` в пресете или `enabled=false` в файле параметров (диагностическое сообщение плана `module.disabled`). Явный запрос приоритетнее файла параметров. Включение и отключение одного и того же модуля в одном запросе — это `OL_E_MODULE_SETTING_INVALID`.
Правила планирования: запрошенный модуль, который не удаётся загрузить, делает план неисполнимым с кодом `OL_E_MODULE_*`. Модуль, вошедший сам (файл параметров, содержимое, по умолчанию) и не поддающийся загрузке, не останавливает сеанс: план сообщает `OL_W_MODULE_NOT_LOADED`.
```text
OmsiLaunch.exe /new /map:maps\Grundorf\global.cfg /entrypoint-index:1 /no-module:example-preload
```
## Параметры
Модуль объявляет свои параметры в `settings`; каждая запись:
| Ключ | Обязателен | Значение |
| --- | --- | --- |
| `key` | да | `[a-z][a-z0-9_]{0,31}`, не `enabled`. |
| `type` | да | `boolean`, `integer`, `choice` или `text`. |
| `default` | да | Значение, когда ничто другое его не задаёт: булево значение, целое число, строка или `"auto"`, если `auto` равно true. |
| `auto` | нет | `true`: значение `auto` принимается и передаётся модулю, который решает во время выполнения. |
| `min`, `max` | нет | Целочисленные границы. |
| `choices` | для `choice` | Допустимые значения (`[a-z0-9][a-z0-9_.-]{0,31}`, не `auto`). |
| `max_length` | нет | Предел длины текста (1..4096, по умолчанию 256). Управляющие символы отклоняются. |
| `flag` | нет | Флаг командной строки для этого параметра, `[a-z][a-z0-9-]{2,31}`: `/:` или просто `/` для `true` у булева параметра. |
| `description` | нет | Не более 256 символов. |
Значения нормализуются: булевы принимают `true`/`false`, `yes`/`no`, `on`/`off`, `1`/`0`; варианты `choice` сопоставляются без учёта регистра; целые числа проверяются по своим границам.
Эффективное значение каждого параметра определяется в таком порядке:
1. запрос: `LaunchSpec.Modules.Settings` (`".": ""`), `/module-setting:.=`, собственный флаг модуля или `modules..settings` в выбранном пресете профиля сеанса. Как и для любого другого поля пресета, командная строка не может одновременно задавать ключ, который задаёт пресет (`OL_E_SESSION_PROFILE_OVERRIDE_CONFLICT`);
2. [файл параметров](#the-settings-file);
3. значение по умолчанию из манифеста.
Недопустимое запрошенное значение или запрошенный ключ, которого модуль не объявлял, делает план неисполнимым (`OL_E_MODULE_SETTING_INVALID`). Запись файла параметров, которую нельзя использовать, игнорируется с `OL_W_MODULE_SETTING_IGNORED`, как и запрошенные параметры модуля, который сеанс не загружает. План перечисляет каждое эффективное значение вместе с его источником (`request`, `settings-file` или `default`) в `PlannedModule.Settings`.
Модуль получает каждый объявленный ключ с его эффективным значением: `IRuntimeModuleContext.Settings`, `get_value("setting.")` в нативном компоненте и объект `settings` контекста сеансовой программы.
### Файл параметров
`\.omsilaunch\modules.ini`, рядом с профилями сеанса. OmsiLaunch его только читает.
```ini
; one section per module id
[example-preload]
enabled=true
ram_budget_mb=8192
monitor=false
```
Секции — это идентификаторы модулей; строки `key=value` задают параметры, `enabled` включает или выключает модуль для каждого сеанса; строки, начинающиеся с `;` или `#`, являются комментариями. Размер файла не более 256 KiB. Всё, что он не может использовать (секция, не являющаяся идентификатором модуля, строка вне секции, неизвестный ключ, недопустимое значение), сообщается с `OL_W_MODULE_SETTING_IGNORED` и игнорируется.
## Сеансовые программы
Модуль может объявить один компонент `host`: свою сеансовую программу, исполняемый файл из собственного каталога. Когда модуль входит в план, контроллер:
1. проверяет закрепление программы и создаёт каталог данных модуля;
2. запускает её до OMSI из каталога модуля с командной строкой `"" --omsilaunch-session `, в приостановленном состоянии, пока она не окажется в своём job (см. [границу безопасности](#security-boundary));
3. записывает контекст первой строкой в её стандартный ввод;
4. ожидает, пока программа не запишет `{"type":"ready"}`, не дольше `prepare_ms`; только после этого запускает OMSI;
5. после создания OMSI записывает `{"type":"omsi-started","pid":}`;
6. в конце сеанса, после выхода OMSI и до восстановления любых файлов, записывает `{"type":"stop"}`, ждёт не дольше `host_stop_ms`, затем завершает программу.
В обоих направлениях используется JSON в UTF-8, по одному объекту в строке. Контекст:
| Поле | Значение |
| --- | --- |
| `type` | `context`. |
| `schema` | `omsilaunch.module-session/v1`. |
| `session_id`, `module_id`, `module_version`, `product_version` | Идентификация. |
| `installation_root`, `module_directory`, `data_directory` | Полные пути. |
| `channel` | Именованный канал (named pipe), который программа может обслуживать для компонентов модуля в OMSI: `\\.\pipe\OmsiLaunch.Module..`. Программа создаёт его с ограничением текущим пользователем и проверяет, что клиент — процесс OMSI. Компоненты читают его через `get_value("session_channel")` или `IRuntimeModuleContext.SessionChannelName`. |
| `settings` | Эффективные параметры модуля. |
| `world` | `mode`, `map`, `situation`, `entrypoint_index`, `entrypoint` сеанса. |
| `session_files` | Файлы относительно установки, которыми владеет транзакция сеанса. Программа не должна держать их открытыми или отображёнными в память: они восстанавливаются, когда сеанс завершается. |
Что программа может записывать помимо `ready`:
| Строка | Действие |
| --- | --- |
| `{"type":"event","name":"","data":{...}}` | Runtime-событие `module..`. |
| `{"type":"notice","data":{"title":"...","text":"..."}}` | Runtime-событие `module..notice` (см. [уведомления](#notices)). |
| `{"type":"error","data":{"code":"OL_E_...","detail":"..."}}` | Диагностическое сообщение сеанса с этим каталогизированным кодом (иначе `OL_E_MODULE_OPERATION_FAILED`). |
Другие строки, строки длиннее 64 KiB и всё после 10,000 строк игнорируются. Контроллер фиксирует `module..host.started`, `host.ready` (с `prepare_ms`), `host.failed`, `host.exited` и `host.killed`.
Программа, которая не готова в течение `prepare_ms`, завершается до готовности или у которой не выполняется закрепление, приводит к сбою подготовки своего модуля: при `fail-session` сеанс не запускается (`OL_E_MODULE_SESSION_FAILED`); при `disable-module` программа завершается, а модуль остаётся вне OMSI; при `degrade` модуль остаётся (и программа продолжает работать, если может). `StartupTimeoutSeconds` сеанса отсчитывается с момента запуска OMSI, поэтому подготовка его не расходует.
## Конфликты с плагинами OMSI
`conflicts.plugins` называет DLL плагинов OMSI, которые не должны работать рядом с модулем, например более старый плагин, устанавливающий те же хуки. При планировании OmsiLaunch читает секцию `[dll]` каждого `plugins\*.opl`; если какой-либо из них загружает конфликтующую DLL, модуль отклоняется с `OL_E_MODULE_CONFLICT`, а в причине указан этот `.opl`. Внутри OMSI модуль, чья конфликтующая DLL всё же загружена, не загружается. OmsiLaunch никогда не изменяет файлы другого плагина; отключить его — решение пользователя.
## Уведомления
Событие модуля с именем `notice` (из `Emit("notice", ...)`, `emit` нативного компонента или строки notice сеансовой программы) показывается треем OmsiLaunch как всплывающее сообщение, в заголовке которого указан модуль. Сохраняются только `title` (не более 63 символов) и `text` (не более 255 символов), управляющие символы удаляются, и ничто в нём не открывается и не выполняется. Сеансовая программа, которая должна проинформировать пользователя до запуска OMSI (когда трей ещё не показан), использует собственное окно.
## Каталог данных
`\.omsilaunch\module-data\\` — записываемый каталог модуля для этой установки: `IRuntimeModuleContext.DataDirectory`, `get_value("data_directory")` и `data_directory` сеансовой программы. Он создаётся при первом использовании. Он не входит в транзакцию сеанса: то, что модуль там записывает, остаётся после сеанса. `/clear-module-data:` удаляет его (и ничего больше); команда отклоняется, пока сеанс владеет установкой, а каталог, достигаемый через ссылку, удаляется как ссылка и никогда не разыменовывается.
## Жизненный цикл
| Шаг | Где | Что происходит |
| --- | --- | --- |
| 1. Обнаружение | контроллер | Читается `plugins\OmsiLaunch.Modules\*`: манифест и целостность (`ListRuntimeModulesAsync`, `/list:RuntimeModules`). |
| 2. Допуск | контроллер, при планировании | Оцениваются модули, которые [входят в сеанс](#choosing-modules-for-a-session): пригодность, протокол моста, возможности, привязка содержимого, конфликты плагинов, параметры. Результат — `SessionPlan.PlannedModules`. |
| 3. Подготовка | контроллер, при запуске | [Сеансовые программы](#session-programs) модулей запускаются и ожидаются. Затем допущенные модули передаются плагину в startup handoff (версия 6): их каталоги, SHA-256 каждого оценённого `module.json` и их эффективные параметры. |
| 4. Arm | плагин, до запуска мира | Каждый модуль повторно проверяется и загружается (управляемый компонент — в собственном контексте загрузки). Его нативный компонент, если есть, загружается и запускается первым, затем вызывается `Arm(context)`. |
| 5. Готовность геймплея | плагин | После `gameplay.entered`, если загруженный модуль объявляет `bridge.hooks.d3d9`, мост начинает наблюдать за устройством Direct3D (`d3d.ready`); затем он вызывает `gameplay_ready` нативного компонента и `GameplayReady()`. Для сеанса NEW_MAP привязанная карта проверяется ещё раз относительно карты, загруженной при запуске. |
| 6. Runtime | плагин | Операции `module..` доходят до `Execute`. |
| 7. Остановка | плагин, затем контроллер | Когда сеанс завершается (или модуль даёт сбой), вызывается `Stop()`. После выхода OMSI сеансовые программы останавливаются. |
| 8. Очистка | плагин | Всё, что отслеживал модуль, освобождается в обратном порядке и проверяется на отсутствие; нативный компонент останавливается последним, а его хуки снимаются; его DLL остаётся загруженной, неактивной, до выхода OMSI (поток ещё может возвращаться через detour). |
Каждый обратный вызов внутри процесса выполняется в потоке UI OMSI, как и собственные runtime-операции продукта. Мост измеряет каждый обратный вызов относительно его бюджета: вызов, вернувшийся с опозданием, приводит к сбою своей фазы (`OL_E_MODULE_PHASE_TIMEOUT`). Мост не может прервать обратный вызов, который никогда не возвращается; такой модуль останавливает OMSI, и сеанс заканчивается по тайм-ауту запуска или через `session stop`, как любой другой сеанс.
## Сбои
Сбой — это исключение или превышение времени на любой фазе или в любой операции, возможность, не разрешённая в процессе, привязка содержимого, не выполняющаяся в процессе, конфликтующий плагин, загруженный в OMSI, или сеансовая программа, которая не стала готовой. Дальнейшее определяет `failure_policy` модуля. Во всех случаях ресурсы модуля освобождаются.
| Политика | Сбой при подготовке, загрузке, arm или готовности геймплея | Сбой в операции | Сбой при остановке |
| --- | --- | --- | --- |
| `fail-session` | Модуль освобождён; `module.session-failure`; сеанс завершается ошибкой `OL_E_MODULE_SESSION_FAILED`, OMSI останавливается, файлы восстанавливаются. Сбой до запуска мира предотвращает запуск. | То же. | Сообщается (`module.failed`); сеанс продолжается до своего конца. |
| `disable-module` | Модуль освобождён и отключён; сеанс продолжается. | То же. | Сообщается; модуль освобождён. |
| `degrade` | Как `disable-module`, за исключением того, что сбой сеансовой программы оставляет модуль в сеансе. | Вызов, давший сбой, возвращает свой код; модуль остаётся активным; `module.degraded` сообщается один раз на каждый код. | Сообщается; модуль освобождён. |
Отключённый или давший сбой модуль отвечает на свои операции кодом `OL_E_MODULE_DISABLED`. Сбои модулей отображаются как диагностические сообщения сеанса (`SessionStatus.Diagnostics`) с кодом `OL_E_MODULE_*` и как runtime-события.
## Очистка
Отслеживается всё, что модуль получает через свой контекст: ресурсы, зарегистрированные через `Track`, потоки, запущенные через `RunBackground`, процессы, запущенные через `StartAuxiliaryProcess`. При остановке или сбое мост отменяет `Stopping`, затем освобождает их в обратном порядке. Вспомогательные процессы также помещаются в объект job с завершением при закрытии, поэтому они заканчиваются вместе с OMSI, даже если OMSI уничтожена принудительно; job сеансовой программы завершает её вместе с сеансом OmsiLaunch, даже если сам OmsiLaunch уничтожен принудительно. Ресурс, всё ещё живой после освобождения (процесс, который не завершился, поток, который не закончился), сообщается как `OL_E_MODULE_CLEANUP_INCOMPLETE`. Потоки или процессы, которые модуль создаёт в обход своего контекста, не отслеживаются.
После аварийного завершения для модулей восстанавливать нечего: они не владеют файлами установки, а их память, потоки и процессы, привязанные к job, заканчиваются вместе с процессом OMSI или процессом OmsiLaunch.
## Нативные компоненты и хуки
Нативный компонент — это 32-разрядная DLL, реализующая интерфейс C из `src/OmsiLaunch.Native.x86/include/omsilaunch_module_native_v1.h`:
| Экспорт | Когда вызывается |
| --- | --- |
| `int omsilaunch_module_native_v1_start(const omsilaunch_bridge_v1*)` | При arm, до управляемого `Arm`. Ненулевое значение приводит к сбою фазы. |
| `int omsilaunch_module_native_v1_gameplay_ready(const omsilaunch_bridge_v1*)` | Необязательный; после входа в геймплей. |
| `void omsilaunch_module_native_v1_stop(void)` | При остановке или сбое, до снятия его хуков. После этого таблица моста отклоняет любые вызовы, а DLL остаётся загруженной, неактивной, до выхода OMSI. |
Таблица моста предоставляет компоненту `request_hook`, `emit` (runtime-события `module..` с JSON-объектом из строк) и, начиная с протокола 2, `get_value`. Компонент читает `size` таблицы, прежде чем использовать `get_value` (`OMSILAUNCH_BRIDGE_V1_SIZE_WITH_GET_VALUE`). `get_value(context, name, buffer, capacity, length)` копирует значение в UTF-8 с завершающим нулём и записывает в `*length` число необходимых байтов без него; возвращает `0`, `1` (аргумент), `2` (неизвестное имя), `11` (после остановки) или `12` (буфер слишком мал). Имена:
| Имя | Значение |
| --- | --- |
| `setting.` | Эффективное значение объявленного параметра. |
| `data_directory` | [Каталог данных](#data-directory) модуля. |
| `module_directory` | Собственный каталог модуля. |
| `session_id` | Идентификатор сеанса. |
| `module_version` | Версия из манифеста. |
| `product_version` | Версия продукта OmsiLaunch. |
| `session_channel` | Pipe [сеансовой программы](#session-programs); неизвестно для модуля без неё. |
Хуки запрашиваются только по символической цели, во время `start` или `gameplay_ready`, для цели, возможность которой объявлена в манифесте:
| Цель | Хуки | Возможность |
| --- | --- | --- |
| `iat:!` | Импорт `Omsi.exe`, например `iat:kernel32.dll!ReadFile`. | `bridge.hooks.iat` |
| `d3d9:device.` | `Reset`, `Present`, `BeginScene` или `EndScene` живого устройства Direct3D 9 (запрашивать в `gameplay_ready`, когда устройство существует). | `bridge.hooks.d3d9` |
`request_hook` возвращает в `*original` функцию для продолжения цепочки (detour обязан её вызывать). Его результат: `0` успех, `1` аргумент, `2` неизвестная цель, `3` место не содержит того, что должно, `4` место перехвачено кем-то вне моста, `5` защита памяти, `6` реестр заполнен, `7` порядок, `8` память, `9` поток выполнял байты, подлежащие исправлению, `10` устройства Direct3D ещё нет, `11` сейчас не разрешено или возможность не объявлена.
Каждый хук, который устанавливает мост, для продукта или для модуля, записывается в единый реестр с указанием владельца (`module:` для модуля):
- Хук устанавливается только поверх значения, которое место должно содержать (импортированная функция; функция модуля с именем `d3d9.dll`, системной среды выполнения или обёртки), либо поверх верхнего хука самого реестра на этом месте. Место, уже изменённое кем-то другим, отклоняется (`4`), но никогда не перезаписывается.
- Хуки на одном месте снимаются по принципу «последним пришёл — первым вышел»; хук, поверх которого стоит другой, остаётся, пока тот не снят.
- Собственный хук продукта на Direct3D `Reset` находится в реестре, поэтому хуки устройства от модулей накладываются поверх него.
- При очистке мост снимает все хуки модуля после возврата из `stop`. Если какой-то не удаётся снять (кто-то другой изменил место), очистка сообщает `OL_E_MODULE_CLEANUP_INCOMPLETE`. DLL нативного компонента никогда не выгружается, пока OMSI работает, поэтому detour, через который ещё возвращается поток, остаётся допустимым.
Движок также поддерживает хуки на прологи функций с трамплином (пока байты меняются, другие потоки приостанавливаются, а точные ожидаемые байты проверяются заранее), но цели-прологи модулям пока не предлагаются: такие цели берутся из каталога целей сборки с проверенными байтами.
## Операции
Операция модуля — это `module..`. Она существует только для сеанса, план которого загрузил модуль (`PlannedModule.Operations`); иначе `ExecuteRuntimeAsync` отвечает `OL_E_MODULE_NOT_LOADED` или `OL_E_MODULE_OPERATION_UNKNOWN`. Обязательные аргументы проверяются, необъявленные аргументы отклоняются (`OL_E_RUNTIME_ARGUMENT_REQUIRED`). Типизированная ошибка модуля сохраняет свой код, если это каталогизированный публичный код; любой другой код становится `OL_E_MODULE_OPERATION_FAILED`, а код модуля передаётся в `Values["module_code"]`. Ключи результата, следующие внутреннему соглашению об именовании, удаляются, как и для любой runtime-операции.
```text
OmsiLaunch.exe /new /map:maps\Grundorf\global.cfg /entrypoint-index:1 /module:null-test
OmsiLaunch.exe /runtime:module.null-test.ping /runtime-arg:text=hello
```
## Написание модуля (контракты)
Управляемый компонент ссылается на `OmsiLaunch.Runtime.Modules` и `OmsiLaunch.Api` и реализует `IRuntimeModule`. Мост привязывает эти две ссылки к своим собственным загруженным сборкам (в каком бы контексте загрузки ни работал плагин), поэтому копия, поставляемая с модулем, игнорируется:
| Член | Когда вызывается |
| --- | --- |
| `void Arm(IRuntimeModuleContext context)` | Шаг 4. Ресурсы приобретаются через контекст. |
| `void GameplayReady()` | Шаг 5. |
| `IReadOnlyDictionary Execute(string operation, IReadOnlyDictionary arguments)` | Шаг 6, для каждой объявленной операции (без префикса `module..`). Для типизированной ошибки следует выбросить `ModuleOperationException(code, message)`. |
| `void Stop()` | Шаг 7. |
`IRuntimeModuleContext` предоставляет `SessionId`, `ModuleId`, `ModuleDirectory`, `Settings`, `DataDirectory`, `ProductVersion`, `SessionChannelName`, `Stopping` (токен отмены), `IsCapabilityAvailable`, `Emit`, `Track`, `RunBackground` и `StartAuxiliaryProcess`. Ничто в нём не раскрывает адреса, исполняемый файл или его идентичность. Компонент должен загружаться 32-разрядным плагином (AnyCPU или x86, .NET 6). Закрепляйте каждый файл, который загружают программы модуля (для программы .NET — её `.dll` рядом с `.exe`), чтобы проверка целостности охватывала и их.
## Известные ограничения
- Нативные компоненты могут перехватывать импорты `Omsi.exe` и методы устройства Direct3D; хуки на прологи функций OMSI и места вызовов модулям пока не предлагаются.
- Проверка привязки содержимого в процессе использует карту, запрошенную при запуске (NEW_MAP); для сохранённой ситуации привязка проверяется при планировании относительно карты этой ситуации.
- Хуки устройства — это хуки таблицы виртуальных методов (vtable). Если другой компонент записывает обратно исходные методы устройства (наблюдалось на регрессионной установке вскоре после начала геймплея), хук устройства перестаёт вызываться и не может быть снят; тогда очистка сообщает `OL_E_MODULE_CLEANUP_INCOMPLETE`, а компонент остаётся загруженным. Метод устройства, уже перехваченный чем-то вне реестра, отклоняется (4); событие отказа называет модуль, которому принадлежит этот метод.
- Модули останавливаются при нормальном выходе из OMSI; остановка сеанса завершает OMSI принудительно, поэтому очистка модулей внутри сеанса происходит только через политику сбоев.
- Уведомления показываются, когда трей существует, то есть после запуска OMSI; для всего, что необходимо сообщить до этого момента, сеансовая программа показывает собственное окно.