Runtime-модули

Документация версии main@63ccaf8Исходник на GitHub

Перевод исходной страницы на английском языке для 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 гарантирует следующее:

  1. Процесс OmsiLaunch никогда не загружает код модулей. Код модуля выполняется внутри OMSI, во вспомогательной программе, запущенной изнутри OMSI, или в сеансовой программе модуля.
  2. Программы, которые OmsiLaunch запускает для модулей, — это файлы собственного каталога модуля, проверенные по закреплению и запущенные с командной строкой, которую задаёт OmsiLaunch. Сеансовая программа выполняется в объекте job, который завершает её вместе с сеансом, не допускает второго процесса (она не может запускать другие программы) и отклоняет изменения системных параметров, настроек дисплея, рабочего стола, а также выход из системы и выключение. Она наследует только два дескриптора своих каналов (pipe).
  3. Всё, что объявляет модуль, остаётся внутри его каталога, а записываемые данные — внутри .omsilaunch\module-data\<id>.
  4. Каналы OmsiLaunch (локальное управление, startup handoff, телеметрия, runtime-команды) остаются ограниченными текущим пользователем и привязанными к сеансу; ни один из них не позволяет вызывающей стороне запустить программу, читать или записывать файлы вне установки либо изменять системные параметры.
  5. OmsiLaunch никогда не повышает права. Модуль работает с правами пользователя, запустившего сеанс.
  6. Текст, поступающий от модуля (уведомления, события, значения), рассматривается как данные: ограничен по длине, без управляющих символов, никогда не выполняется, не открывается и не воспринимается как ссылка.

Возможности#

required_capabilities и optional_capabilities называют либо нативную возможность работающей сборки (столбец нативных возможностей каталога возможностей, например time.read или d3d.texture), либо функцию моста:

Функция мостаЧто получает модуль
bridge.eventsEmit: runtime-события module.<id>.<name>.
bridge.background-threadsRunBackground: отслеживаемые фоновые потоки.
bridge.auxiliary-processesStartAuxiliaryProcess: вспомогательные процессы, привязанные к сеансу.
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-fileenabled=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 сопоставляются без учёта регистра; целые числа проверяются по своим границам.

Эффективное значение каждого параметра определяется в таком порядке:

  1. запрос: LaunchSpec.Modules.Settings ("<module-id>.<key>": "<value>"), /module-setting:<module-id>.<key>=<value>, собственный флаг модуля или modules.<id>.settings в выбранном пресете профиля сеанса. Как и для любого другого поля пресета, командная строка не может одновременно задавать ключ, который задаёт пресет (OL_E_SESSION_PROFILE_OVERRIDE_CONFLICT);
  2. файл параметров;
  3. значение по умолчанию из манифеста.

Недопустимое запрошенное значение или запрошенный ключ, которого модуль не объявлял, делает план неисполнимым (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: свою сеансовую программу, исполняемый файл из собственного каталога. Когда модуль входит в план, контроллер:

  1. проверяет закрепление программы и создаёт каталог данных модуля;
  2. запускает её до OMSI из каталога модуля с командной строкой "<program>" --omsilaunch-session <session id>, в приостановленном состоянии, пока она не окажется в своём job (см. границу безопасности);
  3. записывает контекст первой строкой в её стандартный ввод;
  4. ожидает, пока программа не запишет {"type":"ready"}, не дольше prepare_ms; только после этого запускает OMSI;
  5. после создания OMSI записывает {"type":"omsi-started","pid":<process id>};
  6. в конце сеанса, после выхода OMSI и до восстановления любых файлов, записывает {"type":"stop"}, ждёт не дольше host_stop_ms, затем завершает программу.

В обоих направлениях используется JSON в UTF-8, по одному объекту в строке. Контекст:

ПолеЗначение
typecontext.
schemaomsilaunch.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Эффективные параметры модуля.
worldmode, 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_channelPipe сеансовой программы; неизвестно для модуля без неё.

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