The Permanent Plugin

Documentation for version v0.1.0-beta.3View source on GitHub

OmsiLaunch controls OMSI from inside the OMSI process through a plugin that is installed once, under plugins\OmsiLaunch.*, as part of the product. It is never staged, copied, snapshotted, restored or removed by a session. This page explains what the closure contains, how OMSI loads it, how the host validates it before every start, how host and plugin talk (handoff, telemetry, runtime mailbox), and what the plugin does when OMSI is started without OmsiLaunch. Sources: src/OmsiLaunch.Process/RuntimeDeployment.cs (RuntimeArtifactSet, ReleaseManifest, the three shared-memory stores), src/OmsiLaunch.Plugin/CurrentDnneAdapter.cs, src/OmsiLaunch.Plugin/PluginRuntime.cs, src/OmsiLaunch.Plugin/OmsiLaunch.Plugin.opl, src/OmsiLaunch.Api/StartupHandoff.cs and src/OmsiLaunch.Api/RuntimeControlProtocol.cs.

The closure (9 files)#

File under plugins\Role
OmsiLaunch.Plugin.oplOMSI plugin descriptor. Its content is [dll] followed by OmsiLaunch.PluginNE.dll.
OmsiLaunch.PluginNE.dllNative x86 export shim generated by DNNE 2.0.6. Exports OMSI's plugin ABI (PluginStart, PluginFinalize, AccessVariable, AccessTrigger, AccessStringVariable, AccessSystemVariable) and hosts the .NET runtime. Carries the product version resource.
OmsiLaunch.Plugin.dllManaged plugin (net6.0-windows, x86): CurrentDnneAdapter, PluginRuntime, CurrentRuntimeControl, CurrentTelemetrySink.
OmsiLaunch.Plugin.deps.json.NET dependency manifest for the plugin.
OmsiLaunch.Plugin.runtimeconfig.json.NET runtime configuration (framework Microsoft.NETCore.App 6.0, win-x86).
OmsiLaunch.Api.dllWire formats and public records shared with the host.
OmsiLaunch.Builds.Omsi23004.dllThe build profile: executable fingerprint, globals, object layouts, method addresses.
OmsiLaunch.Interop.dllIn-process memory readers and writers built on the profile.
OmsiLaunch.Native.x86.dllNative bridge (C++): build validation, headless start hook, time apply, MakeVehicle, PlaceRandomBus, internet-texture suppression, D3D9 device access.

RuntimeArtifactSet.Load derives this list from the controller's own plugins\ directory: the four named files (.opl, PluginNE.dll, deps.json, runtimeconfig.json), every OmsiLaunch.*.dll in that directory except OmsiLaunch.PluginNE.dll, and the native bridge. OmsiLaunch.Plugin.dll must be among them. Arbitrary DLLs are never swept into OMSI. The release packager (tools/New-ReleasePackage.ps1) writes exactly the nine files above.

How OMSI loads it#

  1. OMSI enumerates plugins\*.opl and loads the DLL named in OmsiLaunch.Plugin.opl: OmsiLaunch.PluginNE.dll.
  2. The DNNE shim starts the x86 .NET 6 runtime described by OmsiLaunch.Plugin.runtimeconfig.json inside the OMSI process and resolves the managed exports in OmsiLaunch.Plugin.dll.
  3. OMSI calls PluginStart. OMSI may call it more than once during startup; only the first call is honoured (Interlocked.Exchange guard), because a successful start owns a one-shot native hook. Later calls return immediately.
  4. PluginStart first checks OMSILAUNCH_INTERNET_TEXTURES_MODE: when it is Disabled, the native downloader suppression is applied (internet-textures.suppressed, or internet-textures.suppression.failed).
  5. PluginRuntime.Start reads the handoff (below), validates the build in-process, arms the headless start hook, opens the runtime mailbox, and schedules the world start on OMSI's UI thread through a SetTimer callback. No runtime command is ever executed on an IPC worker thread; everything runs on the timer callback, on OMSI's original UI thread.
  6. PluginFinalize kills the timer, shuts the runtime down (NativeD3DShutdown), and restores the internet-texture patch.

The AccessVariable, AccessTrigger, AccessStringVariable and AccessSystemVariable exports are empty; OmsiLaunch does not use OMSI's script-variable plugin channel.

Integrity validation before every start#

RuntimeArtifactSet.ValidateInstalled runs during StartSessionAsync (after early recovery, before the transaction is prepared) and during PlanSessionAsync (presence only, via LoadArtifacts). The plan diagnostic plugin.integrity.reference reports which reference was used.

SituationReferenceCheck per fileErrors
release-manifest.json present beside OmsiLaunch.exe (installed package)manifestThe installed file must exist and its SHA-256 must equal the manifest entry for plugins/<name>OL_E_PERMANENT_PLUGIN_MISSING, OL_E_PERMANENT_PLUGIN_MANIFEST_INCOMPLETE (manifest has no entry for a required file), OL_E_PERMANENT_PLUGIN_HASH_MISMATCH
No manifest (development layout)selfPresence plus self-consistency: the installed file must hash to the file in the controller's own plugins\ directoryOL_E_PERMANENT_PLUGIN_MISSING, OL_E_PERMANENT_PLUGIN_HASH_MISMATCH
Manifest unreadable or malformed (files array missing, entry without path/sha256)OL_E_RELEASE_MANIFEST_INVALID
Closure incomplete in the controller directoryOL_E_RUNTIME_ARTIFACT_MISSING (plan not runnable); the CLI additionally reports OL_E_RUNTIME_INSTALLATION_INCOMPLETE when plugins\OmsiLaunch.Plugin.opl or OmsiLaunch.Native.x86.dll is absent

Only plugins/ entries of the manifest are consumed; the manifest is data, never policy. The manifest is generated by the release packager with the SHA-256 of every staged file. When the controller runs from the OMSI root (the release layout), source and destination are the same directory; without a manifest the check therefore degrades to presence and self-consistency, which is why released packages always carry release-manifest.json. Fix for a mismatch: reinstall the package so plugins\ and release-manifest.json agree.

The plugin validates the build a second time in-process: NativeServices.ValidateBuild accepts only the profile identity Omsi23004_692EBFBF and requires NativeValidateBuild to succeed against the running executable; failure is telemetry plugin.build.invalid, which the host maps to OL_E_BUILD_VALIDATION_FAILED. See compatibility.

Host to plugin: environment variables#

CreateProcessW starts Omsi.exe with the parent environment plus:

VariableValueConsumer
OMSILAUNCH_SESSION_IDSession GUID (D format)CurrentRuntimeControl tags D3D handles with it (N format)
OMSILAUNCH_HANDOFF_NAMEOmsiLaunch.Handoff.<sessionId N>PluginRuntime.Start opens this mapping read-only
OMSILAUNCH_TELEMETRY_NAMEOmsiLaunch.Telemetry.<sessionId N>CurrentTelemetrySink.Emit
OMSILAUNCH_RUNTIME_CHANNELOmsiLaunch.Runtime.<sessionId N>CurrentRuntimeCommandMailbox
OMSILAUNCH_INTERNET_TEXTURES_MODENative, Disabled or OverridePluginStart (only Disabled has an in-process effect)

The three mappings are created by the host before the process starts (CurrentStartupHandoffStore, CurrentTelemetryStore, CurrentRuntimeCommandStore) and disposed when the session's lifecycle task ends. They are named kernel objects with the default DACL of the launching user; any process of the same user can open them (accepted same-user trust model, see known limitations).

The startup handoff#

A memory-mapped, read-only, fixed-layout record (StartupHandoffWire, magic OLSH, version 4; version 3 is still accepted by the reader). The 64-byte header holds the magic, version, header size, total size, session GUID, payload size and the SHA-256 of the payload. The payload holds BuildProfileId, MapIdentity, EntrypointIdentity, SituationIdentity (length-prefixed UTF-8), PresentedEntrypointIndex, WorldMode, flags (HeadlessStart, PlayerVehicleEnabled), DateMode and TimeMode. The plugin re-hashes the payload and rejects any mismatch (plugin.handoff.invalid, host error OL_E_PLUGIN_PROTOCOL_MISMATCH). A mapping larger than 1 MiB or with an inconsistent size is rejected the same way.

The plugin accepts a handoff only when WorldMode is NewMap or SavedSituation, HeadlessStart is set, PlayerVehicleEnabled is clear, both date and time modes are Unset, and a saved situation names its .osn. Anything else is plugin.request.unsupported (host error OL_E_CAPABILITY_UNAVAILABLE). The host always sets HeadlessStart.

Telemetry slot#

OmsiLaunch.Telemetry.<session> is a 4096-byte latest-value slot: length (int32 at 0), sequence (int32 at 4), UTF-8 JSON { "name": ..., "data": { ... } } at 8. The producer invalidates the length, writes the payload, publishes a new sequence, then publishes the length last. The host samples it every 100 ms, treats a sample whose length or sequence changed during the copy as torn and skips it, and processes a sample only when its sequence differs from the last one, so identical consecutive events are still distinct. Events are appended to SessionStatus.RuntimeEvents (bounded to the last 256) and drive the semantic lifecycle (plugin.started, world.starting, gameplay.entered, failures). Because only the latest value is kept, a burst of events faster than the host's 100 ms sampling can lose intermediate events; the plugin defers D3D lifecycle events for 2 s after gameplay.entered so the Running boundary is never masked.

Runtime command mailbox#

OmsiLaunch.Runtime.<session> is a 64 KiB single-flight mailbox: state (int32 at 0: 0 idle, 1 requested, 2 responded), length (int32 at 4), envelope at 8. Envelopes are RuntimeCommandWire records (magic OLRC, version 1, 72-byte header with kind, total length, session GUID, request id, payload length and SHA-256 of the UTF-8 JSON payload). The plugin polls the mailbox from its UI-thread timer (50 ms once the world is loaded), executes the command on that thread, and publishes the response only if the slot still holds the same request id; a request the host abandoned on timeout is never answered. An oversized response is replaced by a typed OL_E_RUNTIME_RESPONSE_TOO_LARGE error. Details and timeouts are in runtime control.

DLL search policy#

OmsiLaunch.Plugin, OmsiLaunch.Interop, OmsiLaunch.Process and the CLI assemblies declare [assembly: DefaultDllImportSearchPaths(DllImportSearchPath.AssemblyDirectory | DllImportSearchPath.System32)]. Native imports (OmsiLaunch.Native.x86.dll, user32.dll, kernel32.dll) resolve from the assembly's own directory (plugins\) or the Windows system directory only; the OMSI root and PATH are never probed. OmsiLaunch.Native.x86.dll is therefore loaded from plugins\ and nowhere else.

When OMSI is started without OmsiLaunch#

Because the closure is permanent, OMSI loads OmsiLaunch.PluginNE.dll on every start, including starts from Steam or the desktop. In that case:

  • OMSILAUNCH_INTERNET_TEXTURES_MODE is absent, so no downloader patch is applied.
  • OMSILAUNCH_HANDOFF_NAME is absent, so PluginRuntime.Start emits plugin.handoff.invalid and returns false. CurrentTelemetrySink.Emit returns immediately when OMSILAUNCH_TELEMETRY_NAME is unset, so nothing is written anywhere.
  • No build validation, no native hook, no mailbox, no timer. The plugin stays loaded but inert; OMSI behaves as if the plugin were not there.
  • PluginFinalize at OMSI exit calls the native restore routine and NativeD3DShutdown, both no-ops when nothing was installed.

The .opl therefore does not need to be removed to run OMSI normally.

Non-interference with third-party plugins#

Sessions never enumerate, hash, copy, remove or restore other files in plugins\. The plugin does not use OMSI's AccessVariable channel and does not touch other plugins' state. The only in-process patches are the profiled headless-start hook (a one-shot VMT redirection armed for the session), the optional internet-texture suppression (restored in PluginFinalize), and the D3D9 device Reset interception used for texture lifecycle tracking.

Difference from OmsiHook#

OmsiLaunch has no runtime dependency on OmsiHook or on any OmsiHook binary: the only packages referenced are DNNE 2.0.6 and YamlDotNet 15.1.2, and there is no using OmsiHook or P/Invoke into OmsiHook DLLs anywhere in the product. What OmsiLaunch does share with OmsiHook is derived knowledge: object layouts and several read wrappers were reconciled from the pinned OmsiHook checkout (space928/Omsi-Extensions, commit 7687b6623f5f74b4419695257bd2a4eef54dd93e, LGPL-3.0-only) against the exact Omsi23004_692EBFBF executable. The attribution and license terms are in THIRD-PARTY-NOTICES.md and the per-file reuse matrix in third_party/OMSIHOOK-REUSE-MATRIX.md. OmsiHook injects a separate process and exposes raw pointers; OmsiLaunch runs in-process, exposes opaque session-scoped handles only, and strips any native address from public results (see capabilities).