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
| Function | Arguments | Remembered across en-target restarts |
|---|---|---|
encore.target.isAvailable() | none | |
encore.target.addGlobalPed(options) | option or list of options | yes |
encore.target.removeGlobalPed(names) | name or list of names | |
encore.target.addGlobalVehicle(options) | option or list | yes |
encore.target.removeGlobalVehicle(names) | name or list | |
encore.target.addGlobalObject(options) | option or list | yes |
encore.target.removeGlobalObject(names) | name or list | |
encore.target.addGlobalPlayer(options) | option or list | yes |
encore.target.removeGlobalPlayer(names) | name or list | |
encore.target.addModel(models, options) | model name, hash or list; option or list | yes |
encore.target.removeModel(models, names) | model or list; name or list | |
encore.target.addLocalEntity(entity, options) | entity handle; option or list | no |
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 exceptaddLocalEntityis remembered in your resource. It is applied now if en-target is started, and applied again each time en-target starts. addLocalEntitydoes 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.
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
| Field | Meaning |
|---|---|
name | Unique id. Defaults to <resource>:<label>. Adding an option with the same name replaces the old one |
label | Text shown in the list. Defaults to name |
icon | An icon name |
distance | Maximum distance in metres. en-target defaults to 2.0 |
bones | A bone name or list. Shows only when looking near one of these bones, and passes the matched bone to your handlers |
groups | Only for players with a job or player group (below) |
items | Only for players carrying items (below) |
canInteract | function(entity, distance, coords, name, bone) returning boolean. Errors hide the option and warn once |
onSelect | function(data), called when picked |
event | If there is no onSelect: TriggerEvent(event, data) |
serverEvent | If there is no onSelect: TriggerServerEvent(serverEvent, data) |
command | If there is no onSelect: ExecuteCommand(command) |
groups
| Form | Shows 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
| Form | Shows 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:
| Field | Meaning |
|---|---|
name | The option's name |
label | The option's label |
entity | The entity handle, if you looked at an entity |
coords | The point you looked at |
distance | Distance from you to that point |
zone | The zone name, for zone options |
bone | The 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
onSelectruns in a new thread in your resource, so it may wait: callbacks, progress bars, dialogs.canInteractruns 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:
- For an entity: that entity's local options, then options for its model, then global options for its type (ped, player, vehicle or object).
- 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:
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)
endSecurity
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.
-- 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
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
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
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:
| Export | Arguments |
|---|---|
AddGlobalPed | options[] |
RemoveGlobalPed | names[] |
AddGlobalVehicle | options[] |
RemoveGlobalVehicle | names[] |
AddGlobalObject | options[] |
RemoveGlobalObject | names[] |
AddGlobalPlayer | options[] |
RemoveGlobalPlayer | names[] |
AddModel | models[], options[] (models may be names or hashes) |
RemoveModel | models[], names[] |
AddLocalEntity | entity, options[] |
RemoveLocalEntity | entity, names[] |
AddSphereZone | zone with { name, coords, radius, options[] } |
RemoveZone | name |
What the library guarantees:
- It always passes lists, never a single option or name.
- Options are copies, and
onSelectis already wrapped to run in a thread in the registering resource. - Remove calls always pass a list, possibly empty, never
nil. (en-target itself treatsnilnames as "remove everything", but the library never sends that.) - Return values are ignored.
What a replacement must do:
- Call
option.onSelect(data)with at leastentity,coordsanddistance, pluszoneandbonewhere they apply. Fall back toevent,serverEvent(withdata.entityas a network id) andcommandwhen there is noonSelect. - Call
canInteract(entity, distance, coords, name, bone)synchronously, and hide the option if it returns false or errors. - Honour
distance,groups,itemsandbones. EndCore resources rely on them to gate options. - Replace an option when one with the same
nameis added again. The library re-sends every remembered registration each timeen-targetstarts. - 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.
-- 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 entitiesOption 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.
-- 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 contractThe 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).
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.