Runtime Control

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

British English edition of the canonical page for OmsiLaunch 0.1.0-beta3. The US English page is normative: where the two differ, it and the code take precedence.

Runtime control is the set of read, write and action operations that OmsiLaunch executes inside a running OMSI session. This page explains the three ways to reach it (owner API, CLI client, local control plane), how requests travel from the caller to the plugin and back, how handles and request identities work, what timeouts apply, what is deliberately not journalled, and the argument and result conventions, with a worked example for each family. The operation inventory itself, with arguments, result keys and errors, is in capabilities. Sources: OmsiLaunchService.ExecuteRuntimeAsync, CurrentRuntimeCommandStore (src/OmsiLaunch.Process/RuntimeDeployment.cs), CurrentRuntimeCommandMailbox and CurrentRuntimeControl (src/OmsiLaunch.Plugin/CurrentRuntimeControl.cs), LocalControlPlane and CliInput (tools/OmsiLaunch.Cli/).

Three entry points#

Entry pointWhoPathTimeout
Owner APIAn integrator that started the session in-process with IOmsiLaunch.StartSessionAsyncExecuteRuntimeAsync(session, RuntimeCommand, timeout) -> registry validation -> session lookup -> mailboxcaller-supplied TimeSpan
Owner CLI (/runtime:<op>)The OmsiLaunch.exe process that owns the sessionone operation executed right after Running, result written to .omsilaunch\diagnostics\<session>-runtime-operation.json and to the console; the session continues5 s (15 s for road-vehicles.spawn)
CLI clientAny OmsiLaunch.exe invocation without an installation argument, for example OmsiLaunch.exe time getnamed pipe runtime.execute to the owner of the installation the executable lives in -> owner calls ExecuteRuntimeAsync8 s (30 s for road-vehicles.spawn) on both the client and the owner side
Local control planeAny process of the same Windows usersame pipe protocol as the CLI client; see local controlas above

Every path ends in ExecuteRuntimeAsync, which enforces the public boundary in this order:

  1. PublicCapabilityRegistry.ValidateRuntimeArguments: an operation not in PublicRuntimeOperationIds (including every internal.* operation) returns Succeeded=false, ErrorCode=OL_E_RUNTIME_OPERATION_UNKNOWN; a missing or blank required argument returns OL_E_RUNTIME_ARGUMENT_REQUIRED. Neither touches the session.
  2. Session lookup (KeyNotFoundException for an unknown handle), OL_E_RUNTIME_SESSION_MISMATCH when RuntimeCommand.SessionId differs from the handle, OL_E_SESSION_NOT_RUNNING unless the state is Running.
  3. Mailbox request (below), then ScrubInternalValues removes any result key starting with internal_ or ending with _address, _pointer, _vmt.

The CLI client and the control plane run step 1 themselves before contacting the owner, so an invalid command name is reported as exit code 2 even when no session is active (OL_E_RUNTIME_OPERATION_UNKNOWN and OL_E_RUNTIME_ARGUMENT_REQUIRED map to InvalidArguments; other rejections to 7; no owner to 4).

The control-plane endpoint exists only while the owner is Running, after startup and after any /runtime-batch, /runtime-write-batch or /d3d-batch harness has completed. Mutating commands (runtime.execute, session.stop) must carry the active session_id; the CLI obtains it from session.status automatically (OL_E_CONTROL_SESSION_MISMATCH otherwise).

Request identity and the mailbox#

A RuntimeCommand(SessionId, RequestId, Operation, Arguments) is serialised into a RuntimeCommandWire envelope (magic OLRC, version 1, 72-byte header, SHA-256 of the JSON payload) and staged in the session's 64 KiB single-flight mailbox (OmsiLaunch.Runtime.<sessionId>). The plugin polls the mailbox every 50 ms on OMSI's UI thread, executes the operation there, and writes the response.

Rules that make the channel robust:

RuleEffect
Single flightOne request at a time per session; the host serialises callers with a semaphore. A slot that is still requested when a new request arrives is OL_E_RUNTIME_CHANNEL_BUSY.
Session bindingThe plugin ignores (and clears) a request whose session GUID is not its own; the host rejects a response whose session or request id does not match (OL_E_RUNTIME_RESPONSE_INVALID).
TimeoutWhen the deadline passes the host resets the slot to idle and throws TimeoutException("OL_E_RUNTIME_REQUEST_TIMEOUT").
Late responseThe plugin publishes a response only if the slot still holds its request id; a response to an abandoned request is dropped. If one nevertheless lands, the next host request finds a stale responded slot, discards it, and proceeds; if that stale response carries the same request id as the new request, the host throws OL_E_RUNTIME_REQUEST_ID_REUSED. Callers must therefore never reuse a request id within a session.
SizeA request larger than the slot is rejected before staging (ArgumentOutOfRangeException). A bounded list result that does not fit is shortened by the plugin (last rows dropped, truncated=true, smaller returned_count); any other oversized response is replaced by a typed OL_E_RUNTIME_RESPONSE_TOO_LARGE error.
Channel closedAfter the session ends, OL_E_RUNTIME_CHANNEL_CLOSED.

Request ids are ulong chosen by the caller. The CLI uses fixed ranges: 10_001 for /runtime:, 50_001+ for forwarded control-plane requests, 1+ for the read batch, 20_000+ for the D3D batch, 30_000+ for D3DRuntimeApi. Integrators should use a monotonically increasing counter per session.

Handles and stale detection#

PrefixTypeIssued byFormat
rv-RoadVehicleroad-vehicles.list, road-vehicles.spawn (created_handle), player-vehicle.readrv- + six decimal digits (rv-000003)
hb-Humanhumans.listhb- + six decimal digits
d3dtex-D3D textured3d.texture.created3dtex-<sessionId N>-<16 hex digits>

Handles are opaque and session-scoped: they are minted by the plugin, never encode an address, and are meaningless in another session. When a handle is resolved, the plugin checks that the address it maps to is still in the live OMSI collection (as of the last list read; road-vehicles.list and humans.list refresh the view) and re-reads an object fingerprint (the Delphi VMT plus the vehicle definition pointer, or the human model index). A mismatch means the native object was destroyed and its address reused: OL_E_RUNTIME_OBJECT_HANDLE_STALE. The same token is returned again for the same live object across repeated list reads. Residual blind spot: an object of the same class and definition recreated at the same address between two list reads cannot be distinguished. D3D handles are validated by the native bridge: an unknown or foreign-session handle is OL_E_D3D_STALE_RESOURCE_HANDLE, a released one OL_E_D3D_RESOURCE_RELEASED, and a device generation change marks textures STALE.

Handles are never native addresses and must never be parsed, compared numerically, persisted across sessions or passed to another session: treat them as opaque strings valid for the session that issued them.

Runtime evidence: a released D3D handle stays rejected after new textures are created and a handle from a previous session is rejected by the next one (runtime closure H02); a D3D device reset invalidates every live texture (OL_E_D3D_STALE_RESOURCE_HANDLE, D01). RoadVehicle and Human stale detection after natural removal (RV-002) has no safe runtime producer (OMSI did not remove objects in the observation windows and no public operation removes one); it is covered offline.

What is not journalled#

Runtime mutations change OMSI's memory only. They are not recorded in the transaction journal and are not restored when the session ends: time.set, camera.set, camera.lock (the policy stops when the plugin shuts down), vehicle.variable.set, road-vehicles.spawn, road-vehicles.place-random and every d3d.texture.* resource. They disappear with the OMSI process, which OmsiLaunch terminates at the end of the session without letting OMSI persist anything (see transactions and recovery). Nothing in the runtime family touches the filesystem.

Argument and result conventions#

  • Arguments are string key/value pairs. On the CLI, --key=value after the command words becomes a runtime argument (OmsiLaunch.exe vehicles get --handle=rv-000001); /runtime-arg:key=value is the equivalent for /runtime:<op>. Numbers use the invariant culture (. decimal separator); booleans are true/false.
  • Command words map to operation ids through CliInput.HierarchicalRoutes (for example time get -> time.read, vehicles summary -> road-vehicles.read, scripts variable set -> vehicle.variable.set). The full route table is in the CLI reference. D3D operations have no command-word route; use /runtime:d3d.status or /runtime:d3d.texture.create --width=8 --height=8 --format=A8R8G8B8.
  • Results are flat string dictionaries. Lists use <row>.<n>.<field> keys (vehicle.0.handle, track.3.filename, name.12) with count, and where bounded, returned_count and truncated.
  • Failures carry Succeeded=false, an OL_E_ ErrorCode and, for plugin-side failures, detail (and native_status for D3D, exception for unexpected faults).

CLI envelope (--json)#

{
  "ok": true,
  "command": "time.read",
  "protocol_version": "0.1",
  "result": {
    "SessionId": "5f641c8d-5828-42a9-b811-5e45b9d05533",
    "RequestId": 50001,
    "Succeeded": true,
    "ErrorCode": null,
    "Values": { "hour": "6", "minute": "31", "second": "12", "day": "20", "month": "9", "year": "2026" }
  }
}

An error envelope is { "ok": false, "command": ..., "protocol_version": "0.1", "error": { "code": "OL_E_...", "category": "...", "message": "..." } }. Without --json the CLI prints the result object as indented JSON or CODE: message.

API envelope#

var result = await launch.ExecuteRuntimeAsync(session,
    new RuntimeCommand(session.SessionId, requestId++, "vehicle.variable.set",
        new Dictionary<string, string> { ["handle"] = "rv-000001", ["name"] = "Refresh_Strings", ["value"] = "1" }),
    TimeSpan.FromSeconds(5));
if (!result.Succeeded) Console.WriteLine(result.ErrorCode);
else Console.WriteLine(result.Values!["value"]);

ExecuteRuntimeAsync throws for boundary violations after registry validation (OL_E_RUNTIME_SESSION_MISMATCH, OL_E_SESSION_NOT_RUNNING, OL_E_RUNTIME_CHANNEL_*, OL_E_RUNTIME_REQUEST_TIMEOUT, OL_E_RUNTIME_RESPONSE_INVALID) and returns Succeeded=false for registry rejections and plugin-side errors.

Worked examples#

All CLI examples assume an owner is running for the installation that contains OmsiLaunch.exe (started with, for example, OmsiLaunch.exe /new /map:maps\Grundorf\global.cfg /entrypoint-index:1, or OmsiLaunch.exe "/saved:situations\Linie 5.osn" when an example needs a player vehicle) and are run from a second console in the same installation.

FamilyCommandWhat it does
SessionOmsiLaunch.exe session status --jsonReads SessionId, State, diagnostics and the bounded runtime event list.
TimeOmsiLaunch.exe time get then OmsiLaunch.exe time set --hour=7 --minute=30Reads the clock; sets it through the profiled SetTime and returns the read-back.
WeatherOmsiLaunch.exe weather get and OmsiLaunch.exe weather actual getReads current and actual/ICAO weather state. OmsiLaunch.exe weather set --wind_speed=1 returns OL_E_RUNTIME_SETTING_NOT_PERSISTENT.
MapOmsiLaunch.exe map getMap identity, name, description, tile count, year range and traffic side.
CameraOmsiLaunch.exe camera set --field_of_view=50 then OmsiLaunch.exe camera lock --family=0 --preset=1 and OmsiLaunch.exe camera unlockWrites FOV; pins the driver family with preset 1 (requires a player vehicle, for example a saved situation); releases the policy.
VehiclesOmsiLaunch.exe vehicles summary, OmsiLaunch.exe vehicles list, OmsiLaunch.exe vehicles get --handle=rv-000001Counts; handles; one snapshot.
SpawnOmsiLaunch.exe vehicles spawn --model=Vehicles\MAN_SD200\MAN_SD77.busCreates one AI RoadVehicle (30 s timeout); returns created_handle. OmsiLaunch.exe vehicles place-random --group=1 invokes PlaceRandomBus.
PlayerOmsiLaunch.exe player getpresent=false in a headless start, otherwise the player snapshot.
HumansOmsiLaunch.exe humans summary, OmsiLaunch.exe humans list, OmsiLaunch.exe humans get --handle=hb-000001Counts; handles; one snapshot.
TimetableOmsiLaunch.exe timetable get, OmsiLaunch.exe timetable tracks list, OmsiLaunch.exe timetable logs listManager counts; bounded track rows; timetable logs.
ScriptsOmsiLaunch.exe scripts variable list --handle=rv-000001, OmsiLaunch.exe scripts variable get --handle=rv-000001 --name=Refresh_Strings, OmsiLaunch.exe scripts variable set --handle=rv-000001 --name=Refresh_Strings --value=1, OmsiLaunch.exe scripts string get --handle=rv-000001 --name=act_routeNumeric variable list/read/write; string variable read.
Constants and curvesOmsiLaunch.exe constants list --handle=rv-000001, OmsiLaunch.exe constants get --handle=rv-000001 --name=antrieb_getr_version, OmsiLaunch.exe curves list --handle=rv-000001, OmsiLaunch.exe curves evaluate --handle=rv-000001 --name=retarder_stufe1 --x=0.5Vehicle constants and curve evaluation. Names are model-specific: use the names returned by the list command (these were listed for the player bus of situations\Linie 5.osn).
HOFOmsiLaunch.exe hof get --handle=rv-000001HOF metadata of the vehicle definition.
Drivers and ticketsOmsiLaunch.exe drivers list, OmsiLaunch.exe tickets getDriver records; ticket pack.
D3DOmsiLaunch.exe /runtime:d3d.texture.create --width=8 --height=8 --format=A8R8G8B8 then OmsiLaunch.exe /runtime:d3d.texture.update --handle=<HANDLE> --width=8 --height=8 --pixels_base64=<BASE64> and OmsiLaunch.exe /runtime:d3d.texture.release --handle=<HANDLE>Texture lifecycle on the render thread; <HANDLE> is the handle printed by create; --pixels_base64 must decode to width * height * 4 bytes (32-bit formats) and at most 48 KiB.
EventsOmsiLaunch.exe events read, OmsiLaunch.exe events watchBounded event list; polling watch (250 ms) until Ctrl+C.
StopOmsiLaunch.exe session stopRequests the canonical stop: OMSI is terminated and the transaction restored by the owner.

Stability#

The channel itself (runtime.command-channel) is STABLE_BETA: wire integrity, session binding, late-response discard and typed oversize rejection are covered offline by runtime-command.wire-guard, runtime-command.session-binding and runtime-command.late-response-ignored, and at runtime by RV-003 (session 5f641c8d), Round A RA-019 (a client abandoned road-vehicles.spawn after 250 ms; the next request succeeded) and the runtime closure channel battery R03 (timeout, cancellation, reused request id, plugin rejection, internal operation, wrong session and missing argument, each followed by a successful request). Per-operation stability is in capabilities.

Channel reuse after failures#

Every terminal path of a runtime request leaves the mailbox reusable: success, a typed error, a malformed, oversized, foreign-session or wrong-request-id response, timeout, caller cancellation and decode failures all end in one cleanup that returns the slot to idle and clears the length and envelope header, so nothing from an earlier request can be read by the next one. A leftover request or response found when a new request starts is cleared first (OL_E_RUNTIME_REQUEST_ID_REUSED if it carries the new request's id). On the plugin side, an exception inside an operation is answered with OL_E_RUNTIME_OPERATION_FAILED, a result larger than the slot with OL_E_RUNTIME_RESPONSE_TOO_LARGE (a fixed, small envelope), a request for another session with OL_E_RUNTIME_SESSION_MISMATCH, and a request the host has abandoned is never answered. These rules are covered offline (runtime-command.terminal-paths-leave-channel-usable, plugin-runtime.oversized-and-abandoned-responses, plugin-runtime.bounded-list-fits-slot); the paths a caller can produce (timeout, cancellation, reused id, typed rejections, late response) also have runtime evidence (RA-019, R03). Corrupt, foreign or oversized responses cannot be produced from outside the product and remain offline-only.