EndCore Framework

Targeting interface

Register look-at interactions with encore.target, every option field and filter, the fallback without en-target, and the contract a replacement targeting resource implements.

Targeting is how players interact with the world: hold Left Alt, look at a zombie corpse, a vehicle or a locker, and pick an option. EndCore resources register those options through encore.target, and the library forwards them to a resource named en-target.

Because registrations go through the library, start order never matters, restarting en-target doesn't lose options, and you can replace en-target with your own targeting resource.

encore.target is client only.

Library API

FunctionArgumentsRemembered across en-target restarts
encore.target.isAvailable()none
encore.target.addGlobalPed(options)option or list of optionsyes
encore.target.removeGlobalPed(names)name or list of names
encore.target.addGlobalVehicle(options)option or listyes
encore.target.removeGlobalVehicle(names)name or list
encore.target.addGlobalObject(options)option or listyes
encore.target.removeGlobalObject(names)name or list
encore.target.addGlobalPlayer(options)option or listyes
encore.target.removeGlobalPlayer(names)name or list
encore.target.addModel(models, options)model name, hash or list; option or listyes
encore.target.removeModel(models, names)model or list; name or list
encore.target.addLocalEntity(entity, options)entity handle; option or listno
encore.target.removeLocalEntity(entity, names)entity handle; name or list
encore.target.addSphereZone(zone){ name, coords, radius, options, debug? }yes
encore.target.removeZone(name)zone name
  • Global options apply to every entity of a type: all non-player peds, all vehicles, all objects or all players.
  • Model options apply to entities with one of the listed models.
  • Local entity options apply to one specific entity.
  • Sphere zones apply to a spot in the world, whether or not an entity is there.

None of these functions return anything. Give every zone a name so you can remove it later.

How registrations are remembered

  • Every add* call except addLocalEntity is remembered in your resource. It is applied now if en-target is started, and applied again each time en-target starts.
  • addLocalEntity does nothing when en-target isn't running, and isn't replayed after a restart, because the entity may no longer exist. Register it again when you need it.
  • remove* calls do nothing when en-target isn't running.
Warning

A remove* call doesn't delete the remembered registration. If en-target restarts later, options you removed come back. For options that should come and go, use canInteract to hide them instead of removing and re-adding.

Passing options and names

Every function accepts a single option (a table with a name or label), a list of options, a single name, or a list of names, wherever it fits. The library copies your options before sending them on.

Option fields

FieldMeaning
nameUnique id. Defaults to <resource>:<label>. Adding an option with the same name replaces the old one
labelText shown in the list. Defaults to name
iconAn icon name
distanceMaximum distance in metres. en-target defaults to 2.0
bonesA bone name or list. Shows only when looking near one of these bones, and passes the matched bone to your handlers
groupsOnly for players with a job or player group (below)
itemsOnly for players carrying items (below)
canInteractfunction(entity, distance, coords, name, bone) returning boolean. Errors hide the option and warn once
onSelectfunction(data), called when picked
eventIf there is no onSelect: TriggerEvent(event, data)
serverEventIf there is no onSelect: TriggerServerEvent(serverEvent, data)
commandIf there is no onSelect: ExecuteCommand(command)

groups

FormShows for
'medic'Players with that job
{ 'medic', 'police' }Any of those jobs
{ medic = 2, police = 0 }Those jobs at or above the grade
'group'Players in any player group
'group:12'Members of player group 12
{ ['group:12'] = 2 }Members of player group 12 at or above grade 2

Jobs and groups are read from exports['en-core']:GetPlayerData(). See Jobs and Groups.

items

FormShows while carrying
'lockpick'At least one lockpick
{ 'lockpick', 'torch' }At least one of each
{ lockpick = 1, scrap = 3 }At least those counts

The check calls encore.inventory.getItemCount on the client.

onSelect data

onSelect, event and serverEvent receive:

FieldMeaning
nameThe option's name
labelThe option's label
entityThe entity handle, if you looked at an entity
coordsThe point you looked at
distanceDistance from you to that point
zoneThe zone name, for zone options
boneThe matched bone, for options with bones

For serverEvent, data.entity is replaced with the entity's network id, or 0 if it has none.

canInteract and onSelect rules

  • onSelect runs in a new thread in your resource, so it may wait: callbacks, progress bars, dialogs.
  • canInteract runs on every look check, many times a second. Keep it fast and never wait inside it.

Resolution order

When a player looks at something, en-target collects options in this order:

  1. For an entity: that entity's local options, then options for its model, then global options for its type (ped, player, vehicle or object).
  2. Then every sphere zone that contains the point being looked at.

Options are removed when the resource that registered them stops.

When en-target isn't running

Nothing is shown and nothing errors. Global, model and zone registrations wait and apply as soon as en-target starts. If an interaction must work regardless, fall back to a prompt and a keybind:

lua
local LOCKER = vec3(452.1, -993.2, 30.7)

if encore.target.isAvailable() then
    encore.target.addSphereZone({
        name = 'my-resource:locker',
        coords = LOCKER,
        radius = 1.5,
        options = { { name = 'my-resource:open', label = 'Open locker', icon = 'box',
            onSelect = function() TriggerServerEvent('my-resource:openLocker') end } },
    })
else
    local open = encore.addKeybind({
        name = 'my_resource_locker',
        description = 'Open locker',
        defaultKey = 'E',
        disabled = true,
        onPressed = function() TriggerServerEvent('my-resource:openLocker') end,
    })

    CreateThread(function()
        while true do
            local near = #(GetEntityCoords(PlayerPedId()) - LOCKER) < 1.5
            if near == open.disabled then
                open:disable(not near)
                if near then
                    encore.showPrompt({ key = open:getCurrentKey(), label = 'Open locker' })
                else
                    encore.hidePrompt()
                end
            end
            Wait(250)
        end
    end)
end

Security

Danger

groups, items, distance and canInteract run on the player's client. They decide what the player sees, not what the player is allowed to do. Any client can trigger your server event directly.

In the server handler for a target action, check again:

  • The network id resolves to an entity that exists (NetworkGetEntityFromNetworkId).
  • The player is close enough to it.
  • The player has the job, group or items the option required.
  • The action isn't being repeated faster than it could be done in game.
lua
-- server
RegisterNetEvent('my-fuel:siphon', function(netId)
    local src = source
    local vehicle = type(netId) == 'number' and NetworkGetEntityFromNetworkId(netId)
    if not vehicle or vehicle == 0 or not DoesEntityExist(vehicle) then return end

    if #(GetEntityCoords(GetPlayerPed(src)) - GetEntityCoords(vehicle)) > 4.0 then return end
    if encore.inventory.getItemCount(src, 'jerrycan') < 1 then return end

    encore.inventory.removeItem(src, 'jerrycan', 1)
    encore.inventory.addItem(src, 'jerrycan_fuel', 1)
end)

Examples

Vehicle option with bones and items

lua
encore.target.addGlobalVehicle({
    name = 'my-fuel:siphon',
    label = 'Siphon fuel',
    icon = 'fuel',
    distance = 2.0,
    bones = { 'petrolcap', 'petroltank' },
    items = { jerrycan = 1, hose = 1 },
    canInteract = function(entity)
        return GetVehicleFuelLevel(entity) > 5.0
    end,
    onSelect = function(data)
        if encore.progress({ label = 'Siphoning', duration = 6000, disable = { move = true } }) then
            TriggerServerEvent('my-fuel:siphon', NetworkGetNetworkIdFromEntity(data.entity))
        end
    end,
})

Job-gated zone

lua
encore.target.addSphereZone({
    name = 'my-resource:locker',
    coords = vec3(452.1, -993.2, 30.7),
    radius = 1.5,
    options = {
        {
            name = 'my-resource:open',
            label = 'Open locker',
            icon = 'box',
            groups = { police = 0 },
            onSelect = function() TriggerServerEvent('my-resource:openLocker') end,
        },
    },
})

Model options with a server event

lua
encore.target.addModel({ 'prop_dumpster_01a', 'prop_dumpster_02a' }, {
    name = 'my-loot:dumpster',
    label = 'Search dumpster',
    icon = 'search',
    distance = 1.8,
    serverEvent = 'my-loot:searchDumpster',   -- receives data with data.entity as a network id
})

Replacing en-target

The contract

The library calls these client exports on a resource named exactly en-target:

ExportArguments
AddGlobalPedoptions[]
RemoveGlobalPednames[]
AddGlobalVehicleoptions[]
RemoveGlobalVehiclenames[]
AddGlobalObjectoptions[]
RemoveGlobalObjectnames[]
AddGlobalPlayeroptions[]
RemoveGlobalPlayernames[]
AddModelmodels[], options[] (models may be names or hashes)
RemoveModelmodels[], names[]
AddLocalEntityentity, options[]
RemoveLocalEntityentity, names[]
AddSphereZonezone with { name, coords, radius, options[] }
RemoveZonename

What the library guarantees:

  • It always passes lists, never a single option or name.
  • Options are copies, and onSelect is already wrapped to run in a thread in the registering resource.
  • Remove calls always pass a list, possibly empty, never nil. (en-target itself treats nil names as "remove everything", but the library never sends that.)
  • Return values are ignored.

What a replacement must do:

  • Call option.onSelect(data) with at least entity, coords and distance, plus zone and bone where they apply. Fall back to event, serverEvent (with data.entity as a network id) and command when there is no onSelect.
  • Call canInteract(entity, distance, coords, name, bone) synchronously, and hide the option if it returns false or errors.
  • Honour distance, groups, items and bones. EndCore resources rely on them to gate options.
  • Replace an option when one with the same name is added again. The library re-sends every remembered registration each time en-target starts.
  • Drop options registered by a resource when that resource stops.

Option 1: ship your targeting resource as en-target

Name your resource en-target, remove the shipped one, and export the functions above.

lua
-- en-target/client/contract.lua
exports('AddGlobalVehicle', function(options)
    for _, option in ipairs(options) do
        MyTarget.addGlobal('vehicle', option)
    end
end)

exports('RemoveGlobalVehicle', function(names)
    for _, name in ipairs(names) do
        MyTarget.removeGlobal('vehicle', name)
    end
end)

exports('AddSphereZone', function(zone)
    MyTarget.addSphere(zone.name, zone.coords, zone.radius or 1.5, zone.options)
    return zone.name
end)

exports('RemoveZone', function(name)
    MyTarget.removeZone(name)
end)

-- ...and the same for peds, objects, players, models and local entities

Option 2: keep your resource's name and add an adapter

Run a small client-only resource named en-target that translates each call into your targeting resource's API. This is the usual route when your targeting resource has a different option shape.

lua
-- en-target/client.lua (the adapter)
local TARGET = 'my_target'

local function convert(option)
    return {
        name = option.name,
        label = option.label,
        icon = option.icon,
        distance = option.distance or 2.0,
        canInteract = option.canInteract,
        action = function(entity, coords, distance)
            if option.onSelect then
                option.onSelect({ name = option.name, label = option.label,
                    entity = entity, coords = coords, distance = distance })
            end
        end,
    }
end

local function convertAll(options)
    local list = {}
    for i, option in ipairs(options) do list[i] = convert(option) end
    return list
end

exports('AddGlobalVehicle', function(options)
    exports[TARGET]:addGlobalVehicle(convertAll(options))
end)

exports('RemoveGlobalVehicle', function(names)
    exports[TARGET]:removeGlobalVehicle(names)
end)

-- ...and the rest of the contract

The my_target export names and option shape above stand in for whatever your targeting resource actually uses. If your resource can't filter by groups or items itself, check them inside the converted canInteract using exports['en-core']:GetPlayerData() and encore.inventory.getItemCount.

You can also answer the contract from inside your own resource with encore.provideExport, for example encore.provideExport('en-target', 'AddGlobalVehicle', fn).

Warning

The library checks GetResourceState('en-target') == 'started' before it applies anything. With provideExport, a resource named en-target must still be started, even an empty one. Don't answer the same export from two places, and never run the shipped en-target alongside your replacement.