Engine AI Objects

This guide covers AI aircraft injection using the Engine client’s AICreate* methods — thin, typed wrappers over the raw SimConnect DLL. Every call maps directly to a single SimConnect API function with no extra abstraction.

Scope: This guide covers the raw Engine-layer AICreate* / AIRemove* / AIRelease* API. Manager-layer wrappers (TrafficParked, TrafficEnroute, TrafficNonATC, fleet tracking) and the pkg/traffic.Fleet abstraction are out of scope — for the high-level Fleet see the Traffic Guide. For complete movement built on top of them, see Departure Taxi, Arrivals & Parking, Injected Ground Movement, Traffic Picture and Traffic Manager.

Aircraft Kinds

SimConnect exposes four creation APIs, each suited to a different role:

Kind Method When to use
Parked ATC AICreateParkedATCAircraft Static ground traffic at a known airport. SimConnect places the aircraft at a free parking spot. No position required.
Enroute ATC AICreateEnrouteATCAircraft Aircraft following an MSFS .pln flight plan. dFlightPlanPosition is a waypoint index plus the fraction along the next leg; in MSFS 2024 it has no effect and the aircraft starts on the ground at the plan’s departure airport (see Enroute Aircraft).
Non-ATC AICreateNonATCAircraft Aircraft at an explicit world position. Required when you need precise gate/apron placement or airborne injection outside the ATC system.
Simulated Object AICreateSimulatedObject Non-aircraft objects: ground vehicles, boats, or any container title that is not an aircraft.

All four return an object ID asynchronously via SIMCONNECT_RECV_ID_ASSIGNED_OBJECT_ID.

The Create-Acknowledge Pattern

Every AICreate* call is fire-and-forget from the caller’s perspective. You supply a requestID that you chose; SimConnect responds with SIMCONNECT_RECV_ID_ASSIGNED_OBJECT_ID carrying both your requestID and the live objectID. Store that objectID — it is what you need for every subsequent operation on the object.

//go:build windows

package main

import (
    "fmt"

    "github.com/mrlm-net/simconnect/pkg/engine"
    "github.com/mrlm-net/simconnect/pkg/types"
)

const reqCreate uint32 = 1000

// Submit the create call — objectID is not yet known.
client.AICreateParkedATCAircraft("FSLTL A320 Air France SL", "N12345", "EGLL", reqCreate)

// In the message loop, wait for the acknowledgement.
for msg := range client.Stream() {
    switch types.SIMCONNECT_RECV_ID(msg.DwID) {

    case types.SIMCONNECT_RECV_ID_ASSIGNED_OBJECT_ID:
        assigned := msg.AsAssignedObjectID()
        if uint32(assigned.DwRequestID) == reqCreate {
            objectID := uint32(assigned.DwObjectID)
            fmt.Printf("Aircraft spawned: objectID=%d\n", objectID)
            // objectID is now valid for AIRemoveObject, AIReleaseControl, etc.
        }
    }
}

AsAssignedObjectID() returns nil when the message is not of that type, so the switch-case guard is sufficient — no additional nil-check is needed inside the case block.

Parked ATC Aircraft

AICreateParkedATCAircraft places an aircraft at a free parking spot at the given airport. SimConnect selects the spot; you do not control the exact gate or ramp position.

Signature:

func (e *Engine) AICreateParkedATCAircraft(
    szContainerTitle string, // Aircraft model container title (exact match required)
    szTailNumber     string, // Tail / registration number shown in cockpit and ATC
    szAirportID      string, // ICAO airport code
    RequestID        uint32, // Your request ID — returned in ASSIGNED_OBJECT_ID
) error

Example — spawn a parked aircraft at Prague:

//go:build windows

package main

import (
    "context"
    "fmt"

    simconnect "github.com/mrlm-net/simconnect"
    "github.com/mrlm-net/simconnect/pkg/engine"
    "github.com/mrlm-net/simconnect/pkg/types"
)

const (
    reqPark uint32 = 1000
)

func main() {
    ctx := context.Background()
    client := simconnect.NewClient("ParkedTraffic", engine.WithContext(ctx))
    if err := client.Connect(); err != nil {
        panic(err)
    }
    defer client.Disconnect()

    // Queue the create call — SimConnect places the aircraft at a free spot.
    client.AICreateParkedATCAircraft(
        "FSLTL A320 Air France SL", // container title
        "AFR123",                   // tail number
        "LKPR",                     // ICAO
        reqPark,
    )

    for msg := range client.Stream() {
        switch types.SIMCONNECT_RECV_ID(msg.DwID) {

        case types.SIMCONNECT_RECV_ID_ASSIGNED_OBJECT_ID:
            a := msg.AsAssignedObjectID()
            if uint32(a.DwRequestID) == reqPark {
                fmt.Printf("Parked: objectID=%d\n", uint32(a.DwObjectID))
            }

        case types.SIMCONNECT_RECV_ID_EXCEPTION:
            ex := msg.AsException()
            fmt.Printf("SimConnect exception %d at index %d\n",
                ex.DwException, ex.DwIndex)
        }
    }
}

Note: The container title must exactly match a model installed in the simulator. The EnumerateSimObjectsAndLiveries API, covered below, lets you discover valid titles at runtime.

Enroute ATC Aircraft

AICreateEnrouteATCAircraft injects an aircraft into the ATC system, following an MSFS .pln flight plan from the given position along the route (waypoint index plus the fraction along the next leg).

MSFS 2024: the position does not take effect. Tested live (#369), every enroute ATC aircraft appeared on the ground at its plan’s departure airport, and the simulator refused one (CREATE_OBJECT_FAILED) whose departure airport it had not loaded. To have an aircraft appear airborne mid-route, create it with AICreateNonATCAircraft and give it the rest of the route as waypoints (traffic.EnrouteStart, see Traffic Manager).

Signature:

func (e *Engine) AICreateEnrouteATCAircraft(
    szContainerTitle  string,  // Aircraft model container title
    szTailNumber      string,  // Tail / registration number
    iFlightNumber     uint32,  // ATC flight number (numeric part)
    szFlightPlanPath  string,  // Absolute path to an MSFS .pln file
    dFlightPlanPosition float64, // Waypoint index + fraction along the next leg (2.5 = halfway 2→3)
    bTouchAndGo      bool,    // Whether the aircraft performs touch-and-go landings
    RequestID        uint32,  // Your request ID
) error

Example — inject an aircraft at the beginning of a plan:

//go:build windows

package main

import (
    "fmt"
    "path/filepath"

    simconnect "github.com/mrlm-net/simconnect"
    "github.com/mrlm-net/simconnect/pkg/engine"
    "github.com/mrlm-net/simconnect/pkg/types"
)

const reqEnroute uint32 = 1001

func spawnEnroute(client engine.Client, planDir string) {
    client.AICreateEnrouteATCAircraft(
        "FSLTL A320 Air France SL",
        "AFR456",
        456,
        filepath.Join(planDir, "LKPREDDN_MFS_NoProc.pln"),
        0.0,   // inject at the start of the plan
        false, // not touch-and-go
        reqEnroute,
    )
}

func handleMessages(client engine.Client) {
    for msg := range client.Stream() {
        switch types.SIMCONNECT_RECV_ID(msg.DwID) {

        case types.SIMCONNECT_RECV_ID_ASSIGNED_OBJECT_ID:
            a := msg.AsAssignedObjectID()
            if uint32(a.DwRequestID) == reqEnroute {
                fmt.Printf("Enroute aircraft: objectID=%d\n", uint32(a.DwObjectID))
            }
        }
    }
}

func main() {
    client := simconnect.NewClient("EnrouteTraffic")
    if err := client.Connect(); err != nil {
        panic(err)
    }
    defer client.Disconnect()

    spawnEnroute(client, `C:\MSFS-TEST-PLANS`)
    handleMessages(client)
}

Note: The flight plan path must be an absolute Windows path to an MSFS-compatible .pln file. Relative paths are not supported by the SimConnect API.

Non-ATC Aircraft at an Explicit Position

AICreateNonATCAircraft gives you full control over initial position, heading, altitude, and airspeed. Use this when you need to place an aircraft at a specific gate, apron stand, or airborne location that is not managed by ATC.

Signature:

func (e *Engine) AICreateNonATCAircraft(
    szContainerTitle string,
    szTailNumber     string,
    initPos          types.SIMCONNECT_DATA_INITPOSITION,
    RequestID        uint32,
) error

SIMCONNECT_DATA_INITPOSITION fields:

Field Type Description
Latitude float64 Decimal degrees, positive = north
Longitude float64 Decimal degrees, positive = east
Altitude float64 Feet MSL (not meters)
Pitch float64 Degrees, positive = nose up
Bank float64 Degrees, positive = right wing down
Heading float64 True degrees (0–360)
OnGround DWORD 1 = on ground, 0 = airborne
Airspeed SIMCONNECT_DATA_INITPOSITION_AIRSPEED (DWORD) Knots. SIMCONNECT_DATA_INITPOSITION_AIRSPEED_CRUISE (0xFFFFFFFF, the SDK’s -1) selects the cruise speed and …_KEEP (0xFFFFFFFE, -2) keeps the current speed

Example — spawn an aircraft at a specific gate:

//go:build windows

package main

import (
    "fmt"

    simconnect "github.com/mrlm-net/simconnect"
    "github.com/mrlm-net/simconnect/pkg/engine"
    "github.com/mrlm-net/simconnect/pkg/types"
)

const reqNonATC uint32 = 1002

func main() {
    client := simconnect.NewClient("NonATCTraffic")
    if err := client.Connect(); err != nil {
        panic(err)
    }
    defer client.Disconnect()

    // Precise gate coordinates obtained from facility data.
    client.AICreateNonATCAircraft(
        "FSLTL A320 Air France SL",
        "N99001",
        types.SIMCONNECT_DATA_INITPOSITION{
            Latitude:  50.1006, // gate lat (decimal degrees)
            Longitude: 14.2601, // gate lon
            Altitude:  1247,    // feet MSL (Prague elevation)
            Heading:   245,     // facing apron
            OnGround:  1,       // parked
            Airspeed:  0,
        },
        reqNonATC,
    )

    for msg := range client.Stream() {
        if types.SIMCONNECT_RECV_ID(msg.DwID) == types.SIMCONNECT_RECV_ID_ASSIGNED_OBJECT_ID {
            a := msg.AsAssignedObjectID()
            if uint32(a.DwRequestID) == reqNonATC {
                fmt.Printf("Non-ATC aircraft: objectID=%d\n", uint32(a.DwObjectID))
            }
        }
    }
}

EX1 Variants — Livery Selection

The EX1 variants of all three aircraft creation methods add a szLivery parameter that lets you specify an exact livery by name rather than accepting the model’s default.

func (e *Engine) AICreateParkedATCAircraftEX1(
    szContainerTitle string,
    szLivery         string, // Exact livery name; empty string = default livery
    szTailNumber     string,
    szAirportID      string,
    RequestID        uint32,
) error

func (e *Engine) AICreateEnrouteATCAircraftEX1(
    szContainerTitle    string,
    szLivery            string,
    szTailNumber        string,
    iFlightNumber       uint32,
    szFlightPlanPath    string,
    dFlightPlanPosition float64,
    bTouchAndGo        bool,
    RequestID          uint32,
) error

func (e *Engine) AICreateNonATCAircraftEX1(
    szContainerTitle string,
    szLivery         string,
    szTailNumber     string,
    initPos          types.SIMCONNECT_DATA_INITPOSITION,
    RequestID        uint32,
) error

Example — parked aircraft with an explicit livery:

//go:build windows

client.AICreateParkedATCAircraftEX1(
    "Boeing 787-10 Asobo",
    "House",   // livery name as returned by EnumerateSimObjectsAndLiveries
    "BOE787",
    "KSEA",
    1003,
)

Pass an empty string ("") for szLivery to use the model default. The EX1 variants are MSFS 2024 additions; on MSFS 2020 they are available from SimConnect SDK version 11 onwards.

Object Management

Removing an Object

AIRemoveObject destroys an AI object and removes it from the simulation. The requestID is for your own correlation; no meaningful acknowledgement is returned for removal.

//go:build windows

if err := client.AIRemoveObject(objectID, reqRemove); err != nil {
    // handle DLL call failure
}

Call this during shutdown to clean up all spawned objects before disconnecting.

Releasing Control

AIReleaseControl transfers ownership of an AI object from your add-on back to the simulator’s AI system. After release, the simulator drives the object autonomously. To give it a route, release it first, then send the waypoint chain with SetDataOnSimObject (pack it with engine.PackWaypoints(wps), unit size engine.WaypointWireSize). The simulator ignores waypoints for an aircraft it still holds.

//go:build windows

// Release AI control — simulator takes over.
client.AIReleaseControl(objectID, reqRelease)

Release, then waypoints, is the correct sequence when you want the aircraft to taxi and depart along your route. pkg/traffic.Fleet does this in ReleaseControl and SetWaypoints.

Assigning a Flight Plan

AISetAircraftFlightPlan assigns or replaces the flight plan on an existing AI aircraft. The object must already exist (use the objectID from ASSIGNED_OBJECT_ID).

//go:build windows

client.AISetAircraftFlightPlan(
    objectID,
    `C:\MSFS-TEST-PLANS\LKPREDDN_MFS_NoProc.pln`,
    reqFlightPlan,
)

Note: AISetAircraftFlightPlan does not return an ASSIGNED_OBJECT_ID. The requestID parameter is present for symmetry with the rest of the AI API but SimConnect does not dispatch a response message for this call.

Model Discovery

Before spawning aircraft you need valid container titles and livery names. EnumerateSimObjectsAndLiveries requests the full list of installed models for a given object type.

Requesting the enumeration:

//go:build windows

const reqEnum uint32 = 9000

client.EnumerateSimObjectsAndLiveries(reqEnum, types.SIMCONNECT_SIMOBJECT_TYPE_AIRCRAFT)

Receiving the results:

SimConnect returns one or more SIMCONNECT_RECV_ID_ENUMERATE_SIMOBJECT_AND_LIVERY_LIST messages. Each contains an array of SIMCONNECT_ENUMERATE_SIMOBJECT_LIVERY entries.

//go:build windows

case types.SIMCONNECT_RECV_ID_ENUMERATE_SIMOBJECT_AND_LIVERY_LIST:
    list := msg.AsSimObjectAndLiveryEnumeration()
    // list is *types.SIMCONNECT_RECV_ENUMERATE_SIMOBJECT_AND_LIVERY_LIST
    for _, entry := range list.Entries() {
        title := engine.BytesToString(entry.AircraftTitle[:])
        livery := engine.BytesToString(entry.LiveryName[:])
        fmt.Printf("Title: %q  Livery: %q\n", title, livery)
    }

AsSimObjectAndLiveryEnumeration() returns nil when the message is not of that type. RgData is a zero-length marker: the batch’s DwArraySize entries follow the 28-byte list header in the message buffer, and Entries() returns them as a slice (addModels in pkg/traffic/world/control.go reads them the same way by hand). SimConnect may send multiple messages for large model sets; collect them all before using the results.

Object type constants for EnumerateSimObjectsAndLiveries:

Constant Value Description
SIMCONNECT_SIMOBJECT_TYPE_ALL 1 All installed objects
SIMCONNECT_SIMOBJECT_TYPE_AIRCRAFT 2 Fixed-wing aircraft only
SIMCONNECT_SIMOBJECT_TYPE_HELICOPTER 3 Helicopters only
SIMCONNECT_SIMOBJECT_TYPE_BOAT 4 Boats and watercraft
SIMCONNECT_SIMOBJECT_TYPE_GROUND 5 Ground vehicles

See Also