OMEN SDK Guide
A complete tutorial for writing mission-app and vehicle-dynamics plugins for the OMEN tactical moving map (built on the Dead Eye engine).
Overview
OMEN plugins are shared libraries (.so on Linux/macOS,
.dll on Windows) loaded at runtime via the
--plugin flag. They communicate with the host through a
stable C API defined in include/omen_sdk.h. No engine
headers, no C++ standard library, no particular compiler required — a
plugin can be written in plain C, Rust, Zig, or anything that can
produce a shared library with C linkage.
What plugins can do
| Capability | API functions |
|---|---|
| Read own-ship position, heading, altitude, speed | omen_get_ship_state |
| Read CoT-driven tracks (blue/red/neutral/unknown) | omen_track_count, omen_get_track |
| Place labeled map markers | omen_add_marker, omen_remove_marker,
omen_clear_plugin_markers |
| Write to the HUD (8 plugin text slots) | omen_set_hud_line |
| Log to the host console | omen_log |
| Share data with other plugins | omen_kv_set, omen_kv_get,
omen_kv_delete |
| Persist settings across runs | omen_config_set, omen_config_get,
omen_config_save |
| Provide a flight dynamics model (replaces GTM/F-16 aero) | OmenDynamicsModel function-pointer table |
| Provide an autopilot/controller (replaces cascade PID) | OmenController function-pointer table |
Build
Plugins link against nothing from the engine — only libc and the SDK header. The canonical build command is:
gcc -shared -fPIC -fvisibility=hidden -O2 -Wall \
-I include/ \
-o my_plugin.so my_plugin.cOn macOS, replace gcc with cc (Apple
Clang). On Windows, use cl /LD or MinGW
gcc -shared.
A minimal Makefile (adapted from the reference plugins):
CC ?= gcc
CFLAGS ?= -O2 -Wall -Wextra -fPIC -fvisibility=hidden
INC := -I../../../include
OUT := my_plugin.so
SRCS := my_plugin.c
OBJS := $(SRCS:.c=.o)
all: $(OUT)
$(OUT): $(OBJS)
$(CC) -shared -o $@ $^
%.o: %.c
$(CC) $(CFLAGS) $(INC) -c $< -o $@
clean:
rm -f $(OUT) $(OBJS)
.PHONY: all cleanRunning
./deadeye --omen --airframe gtm --manhattan \
--plugin path/to/my_plugin.soMultiple --plugin flags are supported. Plugins
initialize in load order and shut down in reverse order.
Plugin Lifecycle
Every plugin shared library must export a single C function:
OMEN_PLUGIN_EXPORT const OmenPlugin* omen_plugin_entry(void);This returns a pointer to a static OmenPlugin struct
that describes the plugin and provides callback function pointers.
Lifecycle hooks
Host startup
│
├─ dlopen("plugin.so")
├─ dlsym("omen_plugin_entry") → OmenPlugin*
├─ Check api_version == OMEN_SDK_API_VERSION
│
├─ MovingMap::initialize() completes
│
├─ on_init(ctx) ← called once; return 0 for success, non-zero to abort
│
├─ [render loop begins]
│ ├─ on_frame(ctx, dt) ← called every render frame with delta time in seconds
│ └─ (repeat)
│
├─ [CoT events arrive]
│ └─ on_cot_rx(ctx, track) ← called from CoT listener thread (if non-NULL)
│
└─ Host exit
├─ on_shutdown(ctx) ← called once; ctx still valid
├─ auto-save dirty config
└─ dlclose()
Any callback can be NULL to opt out. A v1 mission-app plugin
typically provides on_init, on_frame, and
on_shutdown. A v2 vehicle bundle may skip
on_frame entirely and only provide dynamics
and/or controller tables.
v1 vs v2: Mission Apps vs Vehicle Dynamics
The OMEN_SDK_API_VERSION is currently 2. The version
distinction is about capability, not compatibility:
v1 — Mission app plugins
Classic lifecycle-driven plugins that read host state and draw
overlays. They fill in the on_init / on_frame
/ on_shutdown callbacks and leave dynamics and
controller as NULL. Examples: trail recorders, threat
correlators, BDA engines, CoT triage bots.
v2 — Vehicle dynamics plugins
A single .so can provide a complete black-box vehicle: a
flight dynamics model (replaces the built-in GTM/F-16 aero) and/or an
autopilot (replaces the cascade PID). The plugin fills in the
dynamics and/or controller fields of
OmenPlugin with pointers to static function-pointer
tables.
The host's dynamics and GNC factories pick these up when an airframe JSON specifies:
{
"type": "gtm_t2",
"plugin_path": "examples/sdk/simple_arrow/simple_arrow.so",
...
}A plugin can be both v1 and v2 simultaneously — lifecycle hooks plus
vehicle dynamics in one .so.
Data Types
All data crossing the plugin boundary is POD (Plain Old Data) with
explicit field types. No std::string, no
glm::vec, no virtual methods.
OmenContext
Opaque handle the host passes to every callback. Do not dereference —
only pass it back to omen_* host functions.
OmenShipState
Own-ship state snapshot.
| Field | Type | Units | Description |
|---|---|---|---|
lat_deg |
double |
degrees | WGS84 latitude |
lon_deg |
double |
degrees | WGS84 longitude |
alt_ft |
double |
feet | MSL altitude |
heading_deg |
double |
degrees | True heading, 0-360 |
speed_kt |
double |
knots | Ground speed |
pitch_deg |
double |
degrees | Pitch angle |
roll_deg |
double |
degrees | Roll angle |
sim_time_sec |
double |
seconds | Sim time since spawn |
OmenTrack
CoT-driven track.
| Field | Type | Description |
|---|---|---|
uid[64] |
char[] |
Unique identifier |
callsign[32] |
char[] |
Human-readable callsign |
cot_type[24] |
char[] |
CoT type string, e.g. "a-f-A-M-F" |
affiliation |
OmenAffiliation |
See enum below |
lat_deg |
double |
WGS84 latitude (degrees) |
lon_deg |
double |
WGS84 longitude (degrees) |
alt_m |
double |
Altitude (meters) |
last_update_sec |
double |
Sim time of last update |
OmenAffiliation
OMEN_AFFIL_UNKNOWN = 0
OMEN_AFFIL_FRIENDLY = 1
OMEN_AFFIL_NEUTRAL = 2
OMEN_AFFIL_HOSTILE = 3
OMEN_AFFIL_ASSUMED_FRIEND = 4
OMEN_AFFIL_SUSPECT = 5
OMEN_AFFIL_FAKER = 6
OMEN_AFFIL_JOKER = 7OmenMarker
Map overlay marker.
| Field | Type | Units | Description |
|---|---|---|---|
lat_deg |
double |
degrees | WGS84 latitude |
lon_deg |
double |
degrees | WGS84 longitude |
alt_m |
double |
meters | Altitude |
label[64] |
char[] |
— | Text label |
r, g, b, a |
float |
0-1 | RGBA color. Alpha 0 uses host default. |
OmenRigidBodyState
Rigid body state for dynamics plugins. NED frame.
| Field | Type | Units | Description |
|---|---|---|---|
pos_ned[3] |
double[3] |
feet | North, East, Down (+down) |
vel_body[3] |
double[3] |
fps | u, v, w body-frame velocity |
quat[4] |
double[4] |
— | Attitude quaternion (w, x, y, z) |
omega_body[3] |
double[3] |
rad/s | p, q, r body rates |
OmenAtmosphere
Atmospheric properties at the vehicle's altitude.
| Field | Type | Units | Description |
|---|---|---|---|
rho_slug_ft3 |
double |
slug/ft^3 | Air density |
pressure_psf |
double |
lbf/ft^2 | Pressure |
temperature_R |
double |
Rankine | Temperature |
sound_speed_fps |
double |
fps | Speed of sound |
altitude_ft |
double |
feet | MSL altitude |
wind_ned_fps[3] |
double[3] |
fps | Wind vector in NED |
OmenControlInput
Actuator commands.
| Field | Type | Units | Description |
|---|---|---|---|
elevator_rad |
double |
radians | Elevator deflection |
aileron_rad |
double |
radians | Aileron deflection |
rudder_rad |
double |
radians | Rudder deflection |
throttle |
double |
0-1 | Throttle position |
flap |
double |
— | Flap setting |
gear |
double |
— | Gear position |
OmenWrench
Body-frame forces and moments returned by
compute_wrench.
| Field | Type | Units | Description |
|---|---|---|---|
force_body[3] |
double[3] |
lbf | Fx, Fy, Fz |
moment_body[3] |
double[3] |
lbf-ft | L, M, N |
OmenStateEstimate
Navigation estimate fed to controller plugins.
| Field | Type | Units | Description |
|---|---|---|---|
pos_ned[3] |
double[3] |
feet | Position in NED |
vel_body[3] |
double[3] |
fps | Body velocity |
quat[4] |
double[4] |
— | Attitude (w,x,y,z) |
omega_body[3] |
double[3] |
rad/s | Body rates |
t_sec |
double |
seconds | Current sim time |
OmenGuidanceCommand
Guidance setpoint fed to controller plugins. Each channel has a value
and an *_active flag (0 or 1) indicating whether the
autopilot should track that channel.
| Field | Type | Units | Description |
|---|---|---|---|
target_altitude_ft |
double |
feet | Altitude setpoint |
target_heading_deg |
double |
degrees | Heading setpoint |
target_airspeed_kt |
double |
knots | Airspeed setpoint |
target_climb_fpm |
double |
fpm | Climb rate setpoint |
target_bank_deg |
double |
degrees | Bank angle setpoint |
target_pitch_deg |
double |
degrees | Pitch angle setpoint |
alt_active |
int |
0/1 | Altitude hold active |
hdg_active |
int |
0/1 | Heading hold active |
spd_active |
int |
0/1 | Speed hold active |
climb_active |
int |
0/1 | Climb rate hold active |
bank_active |
int |
0/1 | Bank hold active |
pitch_active |
int |
0/1 | Pitch hold active |
OmenMassProperties
Mass and inertia advertised by dynamics plugins.
| Field | Type | Units | Description |
|---|---|---|---|
mass_slug |
double |
slugs | Vehicle mass |
Ixx, Iyy, Izz |
double |
slug-ft^2 | Principal moments of inertia |
Ixz, Ixy, Iyz |
double |
slug-ft^2 | Products of inertia |
cg_body[3] |
double[3] |
feet | CG in body frame |
S_ref_ft2 |
double |
ft^2 | Reference area |
b_ref_ft |
double |
feet | Reference span |
cbar_ref_ft |
double |
feet | Reference mean chord |
OmenDynamicsModel
Function-pointer table for a flight dynamics plugin.
| Field | Type | Description |
|---|---|---|
mass_props |
OmenMassProperties |
Vehicle mass and geometry |
compute_wrench |
function pointer | Compute body-frame forces and moments |
reset |
function pointer or NULL | Called on sim rewind/respawn |
The compute_wrench signature:
void (*compute_wrench)(const OmenRigidBodyState* state,
const OmenAtmosphere* atm,
const OmenControlInput* control,
OmenWrench* out_wrench);The host's rigid-body integrator handles gravity separately. The plugin returns total aero + propulsion + any other forces/moments.
OmenController
Function-pointer table for an autopilot plugin.
| Field | Type | Description |
|---|---|---|
control_rate_hz |
int |
The host gates compute_control to this rate |
compute_control |
function pointer | Compute actuator commands |
reset |
function pointer or NULL | Called on sim rewind/respawn |
The compute_control signature:
void (*compute_control)(const OmenStateEstimate* est,
const OmenGuidanceCommand* cmd,
const OmenAtmosphere* atm,
double t_sec,
double dt_sec,
OmenControlInput* out_control);OmenPlugin
The top-level struct every plugin must provide.
| Field | Type | Description |
|---|---|---|
api_version |
int |
Must equal OMEN_SDK_API_VERSION (currently 2) |
name |
const char* |
Short unique name (used in logs, config filenames) |
description |
const char* |
Human-readable description |
vendor |
const char* |
Organization name |
version |
const char* |
Free-form version string, e.g. "1.0.0" |
on_init |
function pointer | Called once after host init; return 0 for success |
on_frame |
function pointer or NULL | Called every render frame with dt |
on_cot_rx |
function pointer or NULL | Called on CoT event (worker thread) |
on_shutdown |
function pointer or NULL | Called once on host exit |
dynamics |
const OmenDynamicsModel* or NULL |
v2: flight dynamics model |
controller |
const OmenController* or NULL |
v2: autopilot/controller |
API Reference
Own-ship state
void omen_get_ship_state(OmenContext* ctx, OmenShipState* outState);Fills outState with the most recent integrated own-ship
values. Always succeeds. Safe to call from any thread.
Track list
int omen_track_count(OmenContext* ctx);Returns the number of tracks currently in the host's track manager.
int omen_get_track(OmenContext* ctx, int index, OmenTrack* outTrack);Copies track at the given index into outTrack. Returns 1
on success, 0 if out of range. Strings are copied into the fixed-size
buffers.
Map markers
int omen_add_marker(OmenContext* ctx, const OmenMarker* marker);Adds a labeled point marker. Returns a positive marker ID on success, 0 on failure (marker pool exhausted). Markers persist until explicitly removed.
void omen_remove_marker(OmenContext* ctx, int markerId);Removes a single marker by ID. No-op if ID is 0 or unknown.
void omen_clear_plugin_markers(OmenContext* ctx);Removes all markers added by the calling plugin.
HUD
void omen_set_hud_line(OmenContext* ctx, int line_index, const char* text);Sets one of 8 plugin HUD text slots (indices 0-7). Call once per frame with the current text. The slot is recycled across frames.
Logging
void omen_log(OmenContext* ctx, const char* msg);Prints to the host console with a [PLUGIN <name>]
prefix. Safe from any thread.
Shared key-value store
A global (cross-plugin) string-to-string store for inter-plugin communication. Protected by a mutex — safe from any thread.
void omen_kv_set(OmenContext* ctx, const char* key, const char* value);Sets a key-value pair. Overwrites any existing value for that key.
const char* omen_kv_get(OmenContext* ctx, const char* key);Returns the value for a key, or "" if the key does not
exist. The returned pointer is valid until the next call to
omen_kv_get on the same thread.
void omen_kv_delete(OmenContext* ctx, const char* key);Removes a key from the store. No-op if the key does not exist.
Example: sensor plugin publishes, weapons plugin reads:
// Sensor plugin (on_frame):
char buf[32];
snprintf(buf, sizeof(buf), "%.6f", target_lat);
omen_kv_set(ctx, "target_lat", buf);
// Weapons plugin (on_frame):
const char* lat_str = omen_kv_get(ctx, "target_lat");
double lat = atof(lat_str); // "" → 0.0 if not set yetPlugin config persistence
Each plugin gets its own config file at
~/.deadeye/plugins/<plugin_name>.json. The file is
created automatically on first save. Values survive across host
runs.
void omen_config_set(OmenContext* ctx, const char* key, const char* value);Sets a config key-value pair in memory. Marks the config as dirty.
const char* omen_config_get(OmenContext* ctx, const char* key);Returns the value for a config key, or "" if not set. On
first access, lazily loads the config file from disk. The returned
pointer is valid until the next call to omen_config_get on
the same thread.
void omen_config_save(OmenContext* ctx);Writes the in-memory config to
~/.deadeye/plugins/<plugin_name>.json. The host also
auto-saves dirty configs when the plugin's on_shutdown
returns, so explicit saves are only needed for crash resilience.
Example:
static int my_on_init(OmenContext* ctx) {
// Read a setting from a previous run
const char* interval = omen_config_get(ctx, "drop_interval");
if (interval[0] != '\0') {
g_interval = atoi(interval);
}
return 0;
}
static void my_on_shutdown(OmenContext* ctx) {
// Save settings (also auto-saved by host, but explicit is fine)
char buf[16];
snprintf(buf, sizeof(buf), "%d", g_interval);
omen_config_set(ctx, "drop_interval", buf);
omen_config_save(ctx);
}Examples
Minimal plugin skeleton
#include "omen_sdk.h"
#include <stdio.h>
static int my_init(OmenContext* ctx) {
omen_log(ctx, "hello from my_plugin");
return 0;
}
static void my_frame(OmenContext* ctx, double dt) {
(void)dt;
OmenShipState ship;
omen_get_ship_state(ctx, &ship);
char buf[128];
snprintf(buf, sizeof(buf), "ALT: %.0f ft HDG: %.0f",
ship.alt_ft, ship.heading_deg);
omen_set_hud_line(ctx, 0, buf);
}
static void my_shutdown(OmenContext* ctx) {
omen_log(ctx, "goodbye");
}
static const OmenPlugin g_plugin = {
OMEN_SDK_API_VERSION,
"my_plugin",
"Minimal example plugin",
"My Org",
"1.0.0",
my_init,
my_frame,
NULL, /* on_cot_rx */
my_shutdown,
NULL, /* dynamics */
NULL /* controller */
};
OMEN_PLUGIN_EXPORT const OmenPlugin* omen_plugin_entry(void) {
return &g_plugin;
}Marker placement
static void my_frame(OmenContext* ctx, double dt) {
(void)dt;
OmenShipState ship;
omen_get_ship_state(ctx, &ship);
// Drop a red marker at the current position
OmenMarker m = {0};
m.lat_deg = ship.lat_deg;
m.lon_deg = ship.lon_deg;
m.alt_m = ship.alt_ft * 0.3048;
snprintf(m.label, sizeof(m.label), "WP-%d", g_count);
m.r = 1.0f; m.g = 0.2f; m.b = 0.2f; m.a = 0.9f;
int id = omen_add_marker(ctx, &m);
if (id > 0) g_count++;
}Reading tracks
static void my_frame(OmenContext* ctx, double dt) {
(void)dt;
int n = omen_track_count(ctx);
for (int i = 0; i < n; i++) {
OmenTrack t;
if (omen_get_track(ctx, i, &t)) {
if (t.affiliation == OMEN_AFFIL_HOSTILE) {
char buf[128];
snprintf(buf, sizeof(buf), "HOSTILE: %s at %.4f, %.4f",
t.callsign, t.lat_deg, t.lon_deg);
omen_set_hud_line(ctx, 0, buf);
}
}
}
}HUD output
// 8 slots available (indices 0-7), positioned below the built-in HUD.
omen_set_hud_line(ctx, 0, "Line 1: status text");
omen_set_hud_line(ctx, 1, "Line 2: more info");
// Call every frame — slots are recycled, not accumulated.Vehicle dynamics plugin
A dynamics plugin provides a compute_wrench function
that the host's rigid-body integrator calls at every physics step. The
plugin computes total body-frame forces and moments (aero + propulsion).
Gravity is handled by the host.
See examples/sdk/simple_arrow/simple_arrow.c for a
complete working example including both dynamics and controller in one
.so.
Key points:
- Fill in
OmenMassPropertieswith mass, inertia, and reference geometry - Implement
compute_wrench— receives state, atmosphere, controls; writes forces/moments - Units are feet, slugs, pounds-force, radians, seconds throughout
- The
resetfunction pointer can be NULL for stateless models
JSON Config: Airframe plugin_path
To use a plugin's dynamics model and/or controller, create an
airframe JSON with a plugin_path field:
{
"name": "my_vehicle",
"type": "gtm_t2",
"plugin_path": "path/to/my_vehicle.so",
"visual": {
"model_path": "models/my_model.obj",
"heading_offset_deg": 0.0
},
"default_speed_kt": 200.0,
"mass_slug": 10.0,
"inertia_slugft2": {
"Ixx": 1.0,
"Iyy": 20.0,
"Izz": 20.0,
"Ixz": 0.0
},
"S_ft2": 0.5,
"cbar_ft": 0.5,
"b_ft": 0.5,
"initial_state": {
"u_fps": 337.56,
"v_fps": 0.0,
"w_fps": 0.0,
"pos_ned_ft": [0.0, 0.0, 0.0],
"phi_rad": 0.0,
"theta_rad": 0.0,
"psi_rad": 0.0,
"omega_body_rad_s": [0.0, 0.0, 0.0]
},
"origin_alt_ft": 2000.0,
"retrim": false
}The type field is still required as a placeholder for
the spawn code path. The host loads the plugin .so, calls
omen_plugin_entry(), and swaps in the plugin's
dynamics and/or controller tables after
spawn.
Launch with:
./deadeye --omen --airframe path/to/my_vehicle.jsonDebugging
omen_log
Use omen_log(ctx, msg) liberally during development.
Output appears on the host's stdout with a
[PLUGIN <name>] prefix:
[PLUGIN trail_recorder] trail_recorder loaded — drops a marker every sim second
[PLUGIN trail_recorder] trail_recorder shutting down after 42 breadcrumbs
Common pitfalls
API version mismatch. If the plugin was compiled against a different
OMEN_SDK_API_VERSION, the host refuses to load it. Error appears on stderr. Recompile the plugin against the current header.Missing
omen_plugin_entrysymbol. The function must be exported withOMEN_PLUGIN_EXPORT(which appliesvisibility("default")on POSIX). Without it, the symbol is hidden anddlsymfails.Returning non-zero from on_init. The host logs an error and skips all further callbacks for that plugin. The plugin will not receive
on_frameticks.Slow on_frame. The host renders at the display refresh rate. Anything over ~5 ms in
on_framewill stutter the display. Offload heavy work to a background thread and useon_frameonly to read results and update overlays.omen_kv_get / omen_config_get pointer lifetime. The returned
const char*is valid only until the next call to the same function on the same thread. Copy the string if you need to keep it.Marker pool exhaustion.
omen_add_markerreturns 0 if the host's marker pool is full. Check the return value. Useomen_clear_plugin_markersto reclaim all of your plugin's markers.Config file location. Plugin configs live at
~/.deadeye/plugins/<name>.json. The directory is created automatically on first save. IfHOMEis not set, falls back to/tmp.
Thread Safety
| Callback / function | Thread | Notes |
|---|---|---|
on_init |
Render thread | Called after MovingMap::initialize() |
on_frame |
Render thread | Called every frame from MovingMap::update() |
on_shutdown |
Render thread | Called on host exit, reverse load order |
on_cot_rx |
CoT listener thread | Fires on a worker thread — plugin must lock shared state |
omen_get_ship_state |
Any thread | Reads a snapshot, no locking needed by caller |
omen_track_count |
Any thread | Uses host mutex internally |
omen_get_track |
Any thread | Returns a copy of the snapshot |
omen_add_marker |
Render thread | Convention: call from on_init/on_frame |
omen_remove_marker |
Render thread | Convention: call from on_init/on_frame |
omen_clear_plugin_markers |
Render thread | Convention: call from on_init/on_frame |
omen_set_hud_line |
Any thread | Safe from any thread |
omen_log |
Any thread | Safe from any thread |
omen_kv_set/get/delete |
Any thread | Global mutex protects the store |
omen_config_set/get/save |
Any thread | Per-plugin mutex protects the config |
compute_wrench |
Physics thread | Called by the integrator at physics rate |
compute_control |
Physics thread | Called at control_rate_hz |
Key rule: Marker calls assume the caller is on the
render thread (the on_init / on_frame /
on_shutdown convention). All other host functions are safe
from any thread. If your on_cot_rx handler needs to update
shared plugin state that on_frame reads, protect it with
your own mutex.
Reference Plugins
Two complete reference plugins ship with the SDK:
examples/sdk/trail_recorder/
A v1 mission-app plugin (<100 LOC) that drops a labeled cyan breadcrumb marker at the own-ship position once per sim second. Demonstrates:
- Full lifecycle (init, frame, shutdown)
- Reading ship state
- Adding markers
- Writing to the HUD
- Build with
make -C examples/sdk/trail_recorder
examples/sdk/simple_arrow/
A v2 vehicle-dynamics plugin that provides both a flight dynamics model and an autopilot in plain C. The "arrow" is a toy projectile with quadratic drag, constant thrust, and a proportional autopilot. Demonstrates:
OmenDynamicsModelwithcompute_wrenchOmenControllerwithcompute_controlOmenMassPropertiesconfiguration- Combined v1 lifecycle + v2 vehicle bundle
- Build with
make -C examples/sdk/simple_arrow