# 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; для всего, что необходимо сообщить до этого момента, сеансовая программа показывает собственное окно.