Постоянный плагин
Перевод исходной страницы на английском языке для OmsiLaunch 0.1.0-beta3. Нормативной является английская страница: при расхождениях приоритет имеют английская страница и код.
OmsiLaunch управляет OMSI изнутри процесса OMSI с помощью плагина, который устанавливается один раз в plugins\OmsiLaunch.* как часть продукта. Сеанс никогда не размещает, не копирует, не включает в снимок, не восстанавливает и не удаляет его. На этой странице объясняется, что входит в замыкание плагина (набор его файлов), как OMSI его загружает, как хост проверяет его перед каждым запуском, как хост и плагин взаимодействуют (handoff, телеметрия, почтовый ящик (mailbox) runtime) и что делает плагин, когда OMSI запускается без OmsiLaunch. Источники: src/OmsiLaunch.Process/RuntimeDeployment.cs (RuntimeArtifactSet, ReleaseManifest, три хранилища в общей памяти), src/OmsiLaunch.Plugin/CurrentDnneAdapter.cs, src/OmsiLaunch.Plugin/PluginRuntime.cs, src/OmsiLaunch.Plugin/OmsiLaunch.Plugin.opl, src/OmsiLaunch.Api/StartupHandoff.cs и src/OmsiLaunch.Api/RuntimeControlProtocol.cs.
Замыкание (9 файлов)#
Файл в plugins\ | Роль |
|---|---|
OmsiLaunch.Plugin.opl | Дескриптор плагина OMSI. Его содержимое — [dll], за которым следует OmsiLaunch.PluginNE.dll. |
OmsiLaunch.PluginNE.dll | Нативная x86-прослойка экспорта, сгенерированная DNNE 2.0.6. Экспортирует ABI плагинов OMSI (PluginStart, PluginFinalize, AccessVariable, AccessTrigger, AccessStringVariable, AccessSystemVariable) и размещает в процессе среду выполнения .NET. Содержит ресурс версии продукта. |
OmsiLaunch.Plugin.dll | Управляемый плагин (net6.0-windows, x86): CurrentDnneAdapter, PluginRuntime, CurrentRuntimeControl, CurrentTelemetrySink. |
OmsiLaunch.Plugin.deps.json | Манифест зависимостей .NET для плагина. |
OmsiLaunch.Plugin.runtimeconfig.json | Конфигурация среды выполнения .NET (платформа Microsoft.NETCore.App 6.0, win-x86). |
OmsiLaunch.Api.dll | Форматы передачи данных и публичные записи, общие с хостом. |
OmsiLaunch.Builds.Omsi23004.dll | Профиль сборки: отпечаток исполняемого файла, глобальные переменные, раскладки объектов, адреса методов. |
OmsiLaunch.Interop.dll | Средства чтения и записи памяти внутри процесса, построенные на профиле. |
OmsiLaunch.Native.x86.dll | Нативный мост (C++): проверка сборки, хук headless-запуска, применение времени, MakeVehicle, PlaceRandomBus, подавление интернет-текстур, доступ к устройству D3D9. |
RuntimeArtifactSet.Load выводит этот список из собственного каталога plugins\ контроллера: четыре именованных файла (.opl, PluginNE.dll, deps.json, runtimeconfig.json), каждый OmsiLaunch.*.dll в этом каталоге, кроме OmsiLaunch.PluginNE.dll, и нативный мост. Среди них обязательно должен быть OmsiLaunch.Plugin.dll. Произвольные DLL никогда не попадают в OMSI. Упаковщик релиза (tools/New-ReleasePackage.ps1) записывает ровно девять перечисленных выше файлов.
Как OMSI его загружает#
- OMSI перечисляет
plugins\*.oplи загружает DLL, указанную вOmsiLaunch.Plugin.opl:OmsiLaunch.PluginNE.dll. - Прослойка DNNE запускает внутри процесса OMSI среду выполнения x86 .NET 6, описанную в
OmsiLaunch.Plugin.runtimeconfig.json, и разрешает управляемые экспорты вOmsiLaunch.Plugin.dll. - OMSI вызывает
PluginStart. Во время запуска OMSI может вызвать его несколько раз; учитывается только первый вызов (защитаInterlocked.Exchange), поскольку успешный запуск владеет одноразовым нативным хуком. Последующие вызовы сразу возвращают управление. PluginStartсначала проверяетOMSILAUNCH_INTERNET_TEXTURES_MODE: если значение равноDisabled, применяется нативное подавление загрузчика (internet-textures.suppressedилиinternet-textures.suppression.failed).PluginRuntime.Startчитает handoff (см. ниже), проверяет сборку внутри процесса, взводит хук headless-запуска, открывает почтовый ящик runtime и планирует запуск мира в UI-потоке OMSI через обратный вызовSetTimer. Ни одна runtime-команда никогда не выполняется в рабочем потоке IPC; всё выполняется в обратном вызове таймера, в исходном UI-потоке OMSI.PluginFinalizeуничтожает таймер, завершает работу runtime (NativeD3DShutdown) и восстанавливает патч интернет-текстур.
Экспорты AccessVariable, AccessTrigger, AccessStringVariable и AccessSystemVariable пусты; OmsiLaunch не использует канал плагинов OMSI для переменных скриптов.
Проверка целостности перед каждым запуском#
RuntimeArtifactSet.ValidateInstalled выполняется во время StartSessionAsync (после раннего восстановления после сбоя, до подготовки транзакции) и во время PlanSessionAsync (только проверка наличия, через LoadArtifacts). Диагностическое сообщение плана plugin.integrity.reference сообщает, какой эталон был использован.
| Ситуация | Эталон | Проверка каждого файла | Ошибки |
|---|---|---|---|
release-manifest.json находится рядом с OmsiLaunch.exe (установленный пакет) | manifest | Установленный файл должен существовать, а его SHA-256 должен совпадать с записью манифеста для plugins/<name> | OL_E_PERMANENT_PLUGIN_MISSING, OL_E_PERMANENT_PLUGIN_MANIFEST_INCOMPLETE (в манифесте нет записи для обязательного файла), OL_E_PERMANENT_PLUGIN_HASH_MISMATCH |
| Манифеста нет (раскладка разработки) | self | Наличие и самосогласованность: хеш установленного файла должен совпадать с хешем файла в собственном каталоге plugins\ контроллера | OL_E_PERMANENT_PLUGIN_MISSING, OL_E_PERMANENT_PLUGIN_HASH_MISMATCH |
Манифест нечитаем или повреждён (нет массива files, запись без path/sha256) | OL_E_RELEASE_MANIFEST_INVALID | ||
| Замыкание в каталоге контроллера неполное | OL_E_RUNTIME_ARTIFACT_MISSING (план неисполним); CLI дополнительно сообщает OL_E_RUNTIME_INSTALLATION_INCOMPLETE, если отсутствует plugins\OmsiLaunch.Plugin.opl или OmsiLaunch.Native.x86.dll |
Используются только записи манифеста из plugins/; манифест — это данные, а не политика. Манифест генерируется упаковщиком релиза с SHA-256 каждого размещённого файла. Когда контроллер работает из корневого каталога OMSI (раскладка релиза), источник и назначение — один и тот же каталог; поэтому без манифеста проверка сводится к наличию и самосогласованности, и именно поэтому релизные пакеты всегда содержат release-manifest.json. Способ устранения несовпадения: переустановить пакет, чтобы plugins\ и release-manifest.json согласовывались.
Плагин проверяет сборку второй раз внутри процесса: NativeServices.ValidateBuild принимает только идентичность профиля Omsi23004_692EBFBF и требует успешного выполнения NativeValidateBuild для работающего исполняемого файла; при неудаче отправляется телеметрия plugin.build.invalid, которую хост сопоставляет с OL_E_BUILD_VALIDATION_FAILED. См. совместимость.
От хоста к плагину: переменные окружения#
CreateProcessW запускает Omsi.exe с окружением родительского процесса, дополненным следующими переменными:
| Переменная | Значение | Потребитель |
|---|---|---|
OMSILAUNCH_SESSION_ID | GUID сеанса (формат D) | CurrentRuntimeControl помечает им handle D3D (формат N) |
OMSILAUNCH_HANDOFF_NAME | OmsiLaunch.Handoff.<sessionId N> | PluginRuntime.Start открывает это отображение только для чтения |
OMSILAUNCH_TELEMETRY_NAME | OmsiLaunch.Telemetry.<sessionId N> | CurrentTelemetrySink.Emit |
OMSILAUNCH_RUNTIME_CHANNEL | OmsiLaunch.Runtime.<sessionId N> | CurrentRuntimeCommandMailbox |
OMSILAUNCH_INTERNET_TEXTURES_MODE | Native, Disabled или Override | PluginStart (эффект внутри процесса имеет только Disabled) |
Три отображения создаются хостом до запуска процесса (CurrentStartupHandoffStore, CurrentTelemetryStore, CurrentRuntimeCommandStore) и освобождаются, когда завершается задача жизненного цикла сеанса. Это именованные объекты ядра с DACL по умолчанию для запускающего пользователя; любой процесс того же пользователя может их открыть (принятая модель доверия в пределах одного пользователя, см. известные ограничения).
Startup handoff#
Отображаемая в память запись только для чтения с фиксированной раскладкой (StartupHandoffWire, сигнатура OLSH, версия 4; версия 3 по-прежнему принимается читателем). 64-байтовый заголовок содержит сигнатуру, версию, размер заголовка, общий размер, GUID сеанса, размер полезной нагрузки и SHA-256 полезной нагрузки. Полезная нагрузка содержит BuildProfileId, MapIdentity, EntrypointIdentity, SituationIdentity (UTF-8 с префиксом длины), PresentedEntrypointIndex, WorldMode, флаги (HeadlessStart, PlayerVehicleEnabled), DateMode и TimeMode. Плагин заново вычисляет хеш полезной нагрузки и отклоняет любое несовпадение (plugin.handoff.invalid, ошибка хоста OL_E_PLUGIN_PROTOCOL_MISMATCH). Отображение размером более 1 MiB или с несогласованным размером отклоняется так же.
Плагин принимает handoff, только если WorldMode равно NewMap или SavedSituation, HeadlessStart установлен, PlayerVehicleEnabled сброшен, оба режима даты и времени равны Unset, а сохранённая ситуация указывает свой .osn. Всё остальное — plugin.request.unsupported (ошибка хоста OL_E_CAPABILITY_UNAVAILABLE). Хост всегда устанавливает HeadlessStart.
Слот телеметрии#
OmsiLaunch.Telemetry.<session> — слот последнего значения размером 4096 байт: length (int32 по смещению 0), sequence (int32 по смещению 4), UTF-8 JSON { "name": ..., "data": { ... } } по смещению 8. Производитель делает длину недействительной, записывает полезную нагрузку, публикует новый номер последовательности и последней публикует длину. Хост считывает слот каждые 100 ms, считает образец, у которого длина или номер последовательности изменились во время копирования, «разорванным» и пропускает его, а обрабатывает образец, только если его номер последовательности отличается от предыдущего, поэтому одинаковые последовательные события всё равно различаются. События добавляются в SessionStatus.RuntimeEvents (ограничено последними 256) и управляют семантическим жизненным циклом (plugin.started, world.starting, gameplay.entered, ошибки). Поскольку хранится только последнее значение, серия событий быстрее 100-миллисекундного опроса хоста может привести к потере промежуточных событий; плагин откладывает события жизненного цикла D3D на 2 s после gameplay.entered, чтобы граница Running никогда не маскировалась.
Почтовый ящик runtime-команд#
OmsiLaunch.Runtime.<session> — почтовый ящик на 64 KiB с одним запросом в полёте: state (int32 по смещению 0: 0 — простой, 1 — запрошено, 2 — дан ответ), length (int32 по смещению 4), конверт по смещению 8. Конверты — это записи RuntimeCommandWire (сигнатура OLRC, версия 1, 72-байтовый заголовок с видом, общей длиной, GUID сеанса, идентификатором запроса, длиной полезной нагрузки и SHA-256 полезной нагрузки UTF-8 JSON). Плагин опрашивает почтовый ящик из своего таймера в UI-потоке (50 ms после загрузки мира), выполняет команду в этом потоке и публикует ответ, только если слот всё ещё содержит тот же идентификатор запроса; на запрос, от которого хост отказался по тайм-ауту, ответ никогда не даётся. Слишком большой ответ заменяется типизированной ошибкой OL_E_RUNTIME_RESPONSE_TOO_LARGE. Подробности и тайм-ауты приведены в разделе runtime-управление.
Политика поиска DLL#
OmsiLaunch.Plugin, OmsiLaunch.Interop, OmsiLaunch.Process и сборки CLI объявляют [assembly: DefaultDllImportSearchPaths(DllImportSearchPath.AssemblyDirectory | DllImportSearchPath.System32)]. Нативные импорты (OmsiLaunch.Native.x86.dll, user32.dll, kernel32.dll) разрешаются только из собственного каталога сборки (plugins\) или из системного каталога Windows; корневой каталог OMSI и PATH никогда не просматриваются. Поэтому OmsiLaunch.Native.x86.dll загружается из plugins\ и ниоткуда больше.
Когда OMSI запускается без OmsiLaunch#
Поскольку замыкание постоянно, OMSI загружает OmsiLaunch.PluginNE.dll при каждом запуске, включая запуски из Steam или с рабочего стола. В этом случае:
OMSILAUNCH_INTERNET_TEXTURES_MODEотсутствует, поэтому патч загрузчика не применяется.OMSILAUNCH_HANDOFF_NAMEотсутствует, поэтомуPluginRuntime.Startотправляетplugin.handoff.invalidи возвращаетfalse.CurrentTelemetrySink.Emitсразу возвращает управление, еслиOMSILAUNCH_TELEMETRY_NAMEне задана, поэтому ничего никуда не записывается.- Нет проверки сборки, нет нативного хука, нет почтового ящика, нет таймера. Плагин остаётся загруженным, но бездействует; OMSI ведёт себя так, будто плагина нет.
PluginFinalizeпри завершении OMSI вызывает нативную процедуру восстановления иNativeD3DShutdown; обе ничего не делают, если ничего не было установлено.
Поэтому для обычного запуска OMSI удалять .opl не нужно.
Невмешательство в сторонние плагины#
Сеансы никогда не перечисляют, не хешируют, не копируют, не удаляют и не восстанавливают другие файлы в plugins\. Плагин не использует канал AccessVariable OMSI и не затрагивает состояние других плагинов. Единственные патчи внутри процесса — профилированный хук headless-запуска (одноразовое перенаправление VMT, взводимое для сеанса), необязательное подавление интернет-текстур (восстанавливается в PluginFinalize) и перехват Reset устройства D3D9, используемый для отслеживания жизненного цикла текстур.
Отличие от OmsiHook#
OmsiLaunch не имеет зависимости в runtime от OmsiHook или от каких-либо двоичных файлов OmsiHook: единственные подключённые пакеты — DNNE 2.0.6 и YamlDotNet 15.1.2, и нигде в продукте нет ни using OmsiHook, ни P/Invoke в DLL OmsiHook. Общее у OmsiLaunch с OmsiHook — производные знания: раскладки объектов и несколько обёрток чтения были сверены по зафиксированной копии OmsiHook (space928/Omsi-Extensions, коммит 7687b6623f5f74b4419695257bd2a4eef54dd93e, LGPL-3.0-only) с точным исполняемым файлом Omsi23004_692EBFBF. Указание авторства и условия лицензии приведены в THIRD-PARTY-NOTICES.md, а пофайловая матрица повторного использования — в third_party/OMSIHOOK-REUSE-MATRIX.md. OmsiHook использует внедрение отдельного процесса и предоставляет сырые указатели; OmsiLaunch работает внутри процесса, предоставляет только непрозрачные handle с областью действия сеанса и удаляет любые нативные адреса из публичных результатов (см. возможности).