Runtime-модули
Перевод исходной страницы на английском языке для OmsiLaunch 0.1.0-beta4. Нормативной является английская страница: при расхождениях приоритет имеют английская страница и код.
Runtime-модуль добавляет поведение в управляемый сеанс OMSI: функцию, которая работает вместе с сеансом (например, HUD карты или оптимизации загрузки), не становясь частью самого OmsiLaunch. Мост загружает модуль, предоставляет ему только то, что модуль объявил, и освобождает всё, чем модуль владеет, когда сеанс завершается или модуль даёт сбой. Эта страница — нормативный справочник по runtime-модулям в том виде, в каком они реализованы в src/OmsiLaunch.Runtime.Modules (манифест, параметры, допуск, хост жизненного цикла), в планировщике, в сеансовых программах контроллера (ModuleSessionPrograms, ModuleSessionProcess), в среде модулей плагина и во флагах модулей CLI.
Стабильность: EXPERIMENTAL. Протокол моста 1 проверен в runtime в раунде B (управляемые модули, активация по содержимому, операции, disable-module и fail-session; нативные компоненты с IAT-хуками; см. состояние проверки в runtime). Протокол моста 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. Гарантируется граница безопасности: модули никогда не выполняются внутри процесса OmsiLaunch, и ничто из предлагаемого OmsiLaunch не позволяет модулю выйти за пределы собственных прав OMSI.
Где находятся модули#
Каждый модуль — это каталог ниже каталога постоянного плагина:
<OMSI root>\plugins\OmsiLaunch.Modules\<id>\module.json
<OMSI root>\plugins\OmsiLaunch.Modules\<id>\<managed component>.dll
<OMSI root>\plugins\OmsiLaunch.Modules\<id>\<assets and other components>Имя каталога должно совпадать с id модуля. Модули устанавливает тот, кто их распространяет; сеанс никогда не записывает, не снимает snapshot и не восстанавливает там файлы, а модули не изменяют файлы установки через OmsiLaunch (в манифесте нет способа объявить overlay установки). Записываемые данные модуля находятся в его каталоге данных.
Манифест (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): загружается в каждом сеансе, если не отключён (см. выбор модулей); не может иметь content_binding. |
required_capabilities | нет | Возможности (capability), без которых модуль работать не может (см. возможности). |
optional_capabilities | нет | Возможности, которые модуль использует, когда они доступны; он проверяет их через IsCapabilityAvailable. |
content_binding | нет | { "map": "maps\\<folder>\\global.cfg", "files": [ { "path", "sha256" } ] }: модуль работает только на этой карте и только при неизменных указанных файлах. |
assets | нет | Файлы, которые читает модуль, каждый — { "path", "sha256" }. |
components | да | Ровно один компонент managed { "kind": "managed", "path": "<file>.dll", "sha256", "entry": "<Namespace.Type>" }; при необходимости один компонент native (.dll), компоненты auxiliary (.exe) и, с протоколом 2, один компонент host (.exe, сеансовая программа модуля). |
failure_policy | да | fail-session, disable-module или degrade (см. сбои). |
operations | нет | Операции, которые модуль предоставляет, пока сеанс работает: { "id", "kind": "read"|"write"|"action", "arguments": [ { "name", "required" } ], "timeout_ms" }. |
settings | нет | Протокол 2. Параметры, которые принимает модуль (см. параметры). |
conflicts | нет | Протокол 2. { "plugins": [ "<file>.dll" ] }: DLL плагинов OMSI, которые не должны работать рядом с модулем (см. конфликты). |
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 не может закрепить сам себя.
{
"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 гарантирует следующее:
- Процесс OmsiLaunch никогда не загружает код модулей. Код модуля выполняется внутри OMSI, во вспомогательной программе, запущенной изнутри OMSI, или в сеансовой программе модуля.
- Программы, которые OmsiLaunch запускает для модулей, — это файлы собственного каталога модуля, проверенные по закреплению и запущенные с командной строкой, которую задаёт OmsiLaunch. Сеансовая программа выполняется в объекте job, который завершает её вместе с сеансом, не допускает второго процесса (она не может запускать другие программы) и отклоняет изменения системных параметров, настроек дисплея, рабочего стола, а также выход из системы и выключение. Она наследует только два дескриптора своих каналов (pipe).
- Всё, что объявляет модуль, остаётся внутри его каталога, а записываемые данные — внутри
.omsilaunch\module-data\<id>. - Каналы OmsiLaunch (локальное управление, startup handoff, телеметрия, runtime-команды) остаются ограниченными текущим пользователем и привязанными к сеансу; ни один из них не позволяет вызывающей стороне запустить программу, читать или записывать файлы вне установки либо изменять системные параметры.
- OmsiLaunch никогда не повышает права. Модуль работает с правами пользователя, запустившего сеанс.
- Текст, поступающий от модуля (уведомления, события, значения), рассматривается как данные: ограничен по длине, без управляющих символов, никогда не выполняется, не открывается и не воспринимается как ссылка.
Возможности#
required_capabilities и optional_capabilities называют либо нативную возможность работающей сборки (столбец нативных возможностей каталога возможностей, например time.read или d3d.texture), либо функцию моста:
| Функция моста | Что получает модуль |
|---|---|
bridge.events | Emit: runtime-события module.<id>.<name>. |
bridge.background-threads | RunBackground: отслеживаемые фоновые потоки. |
bridge.auxiliary-processes | StartAuxiliaryProcess: вспомогательные процессы, привязанные к сеансу. |
bridge.hooks.iat | Нативный компонент: хуки на импорты Omsi.exe (iat:<dll>!<function>). |
bridge.hooks.d3d9 | Нативный компонент: хуки на методы живого устройства Direct3D 9 (d3d9:device.<method>). Требует нативной возможности d3d.texture. |
Имя, не относящееся ни к тому, ни к другому, отклоняется при планировании (OL_E_MODULE_CAPABILITY_UNRESOLVED). В сборке OMSI 2 без известной раскладки нативная возможность, которую не разрешает исполняемый файл, определяется в процессе; если и там она не разрешена, модуль не загружается.
Выбор модулей для сеанса#
Модуль входит в сеанс одним из четырёх способов; какой именно, сообщает PlannedModule.Activation:
| Активация | Когда |
|---|---|
requested | Его запрашивает сеанс: LaunchSpec.Modules.Enable, /module:<id> или enabled: true в выбранном пресете профиля сеанса. |
settings-file | enabled=true в его секции файла параметров. |
content | Его активация — content, и сеанс запускает его привязанную карту. |
default | Его активация — default. |
Модуль не допускается в сеанс через LaunchSpec.Modules.Disable, /no-module:<id>, enabled: false в пресете или enabled=false в файле параметров (диагностическое сообщение плана module.disabled). Явный запрос приоритетнее файла параметров. Включение и отключение одного и того же модуля в одном запросе — это OL_E_MODULE_SETTING_INVALID.
Правила планирования: запрошенный модуль, который не удаётся загрузить, делает план неисполнимым с кодом OL_E_MODULE_*. Модуль, вошедший сам (файл параметров, содержимое, по умолчанию) и не поддающийся загрузке, не останавливает сеанс: план сообщает OL_W_MODULE_NOT_LOADED.
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}: /<flag>:<value> или просто /<flag> для true у булева параметра. |
description | нет | Не более 256 символов. |
Значения нормализуются: булевы принимают true/false, yes/no, on/off, 1/0; варианты choice сопоставляются без учёта регистра; целые числа проверяются по своим границам.
Эффективное значение каждого параметра определяется в таком порядке:
- запрос:
LaunchSpec.Modules.Settings("<module-id>.<key>": "<value>"),/module-setting:<module-id>.<key>=<value>, собственный флаг модуля илиmodules.<id>.settingsв выбранном пресете профиля сеанса. Как и для любого другого поля пресета, командная строка не может одновременно задавать ключ, который задаёт пресет (OL_E_SESSION_PROFILE_OVERRIDE_CONFLICT); - файл параметров;
- значение по умолчанию из манифеста.
Недопустимое запрошенное значение или запрошенный ключ, которого модуль не объявлял, делает план неисполнимым (OL_E_MODULE_SETTING_INVALID). Запись файла параметров, которую нельзя использовать, игнорируется с OL_W_MODULE_SETTING_IGNORED, как и запрошенные параметры модуля, который сеанс не загружает. План перечисляет каждое эффективное значение вместе с его источником (request, settings-file или default) в PlannedModule.Settings.
Модуль получает каждый объявленный ключ с его эффективным значением: IRuntimeModuleContext.Settings, get_value("setting.<key>") в нативном компоненте и объект settings контекста сеансовой программы.
Файл параметров#
<OMSI root>\.omsilaunch\modules.ini, рядом с профилями сеанса. OmsiLaunch его только читает.
; 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: свою сеансовую программу, исполняемый файл из собственного каталога. Когда модуль входит в план, контроллер:
- проверяет закрепление программы и создаёт каталог данных модуля;
- запускает её до OMSI из каталога модуля с командной строкой
"<program>" --omsilaunch-session <session id>, в приостановленном состоянии, пока она не окажется в своём job (см. границу безопасности); - записывает контекст первой строкой в её стандартный ввод;
- ожидает, пока программа не запишет
{"type":"ready"}, не дольшеprepare_ms; только после этого запускает OMSI; - после создания OMSI записывает
{"type":"omsi-started","pid":<process id>}; - в конце сеанса, после выхода 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.<session id without dashes>.<module id>. Программа создаёт его с ограничением текущим пользователем и проверяет, что клиент — процесс OMSI. Компоненты читают его через get_value("session_channel") или IRuntimeModuleContext.SessionChannelName. |
settings | Эффективные параметры модуля. |
world | mode, map, situation, entrypoint_index, entrypoint сеанса. |
session_files | Файлы относительно установки, которыми владеет транзакция сеанса. Программа не должна держать их открытыми или отображёнными в память: они восстанавливаются, когда сеанс завершается. |
Что программа может записывать помимо ready:
| Строка | Действие |
|---|---|
{"type":"event","name":"<name>","data":{...}} | Runtime-событие module.<id>.<name>. |
{"type":"notice","data":{"title":"...","text":"..."}} | Runtime-событие module.<id>.notice (см. уведомления). |
{"type":"error","data":{"code":"OL_E_...","detail":"..."}} | Диагностическое сообщение сеанса с этим каталогизированным кодом (иначе OL_E_MODULE_OPERATION_FAILED). |
Другие строки, строки длиннее 64 KiB и всё после 10,000 строк игнорируются. Контроллер фиксирует module.<id>.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 (когда трей ещё не показан), использует собственное окно.
Каталог данных#
<OMSI root>\.omsilaunch\module-data\<id>\ — записываемый каталог модуля для этой установки: IRuntimeModuleContext.DataDirectory, get_value("data_directory") и data_directory сеансовой программы. Он создаётся при первом использовании. Он не входит в транзакцию сеанса: то, что модуль там записывает, остаётся после сеанса. /clear-module-data:<id> удаляет его (и ничего больше); команда отклоняется, пока сеанс владеет установкой, а каталог, достигаемый через ссылку, удаляется как ссылка и никогда не разыменовывается.
Жизненный цикл#
| Шаг | Где | Что происходит |
|---|---|---|
| 1. Обнаружение | контроллер | Читается plugins\OmsiLaunch.Modules\*: манифест и целостность (ListRuntimeModulesAsync, /list:RuntimeModules). |
| 2. Допуск | контроллер, при планировании | Оцениваются модули, которые входят в сеанс: пригодность, протокол моста, возможности, привязка содержимого, конфликты плагинов, параметры. Результат — SessionPlan.PlannedModules. |
| 3. Подготовка | контроллер, при запуске | Сеансовые программы модулей запускаются и ожидаются. Затем допущенные модули передаются плагину в 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.<id>.<operation> доходят до 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.<id>.<name> с 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.<key> | Эффективное значение объявленного параметра. |
data_directory | Каталог данных модуля. |
module_directory | Собственный каталог модуля. |
session_id | Идентификатор сеанса. |
module_version | Версия из манифеста. |
product_version | Версия продукта OmsiLaunch. |
session_channel | Pipe сеансовой программы; неизвестно для модуля без неё. |
Хуки запрашиваются только по символической цели, во время start или gameplay_ready, для цели, возможность которой объявлена в манифесте:
| Цель | Хуки | Возможность |
|---|---|---|
iat:<dll>!<function> | Импорт Omsi.exe, например iat:kernel32.dll!ReadFile. | bridge.hooks.iat |
d3d9:device.<method> | 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:<id> для модуля):
- Хук устанавливается только поверх значения, которое место должно содержать (импортированная функция; функция модуля с именем
d3d9.dll, системной среды выполнения или обёртки), либо поверх верхнего хука самого реестра на этом месте. Место, уже изменённое кем-то другим, отклоняется (4), но никогда не перезаписывается. - Хуки на одном месте снимаются по принципу «последним пришёл — первым вышел»; хук, поверх которого стоит другой, остаётся, пока тот не снят.
- Собственный хук продукта на Direct3D
Resetнаходится в реестре, поэтому хуки устройства от модулей накладываются поверх него. - При очистке мост снимает все хуки модуля после возврата из
stop. Если какой-то не удаётся снять (кто-то другой изменил место), очистка сообщаетOL_E_MODULE_CLEANUP_INCOMPLETE. DLL нативного компонента никогда не выгружается, пока OMSI работает, поэтому detour, через который ещё возвращается поток, остаётся допустимым.
Движок также поддерживает хуки на прологи функций с трамплином (пока байты меняются, другие потоки приостанавливаются, а точные ожидаемые байты проверяются заранее), но цели-прологи модулям пока не предлагаются: такие цели берутся из каталога целей сборки с проверенными байтами.
Операции#
Операция модуля — это module.<id>.<operation>. Она существует только для сеанса, план которого загрузил модуль (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-операции.
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<string, string> Execute(string operation, IReadOnlyDictionary<string, string> arguments) | Шаг 6, для каждой объявленной операции (без префикса module.<id>.). Для типизированной ошибки следует выбросить 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; для всего, что необходимо сообщить до этого момента, сеансовая программа показывает собственное окно.