API
SL-Interact is used from your own scripts through client exports. Every interaction belongs to the resource that added it and is removed automatically when that resource stops.
The examples below are taken from the example resource that comes with SL-Interact (sl_interact_demo, next to sl_interact in the download). Its code is open: put it in your resources folder, start it and type /slidemo to spawn every example in front of you.
Quick start
Section titled “Quick start”local SI = exports.sl_interact
local id = SI:addCoords(vec3(147.6, -1035.8, 29.3), { label = 'ATM', icon = 'cash', options = { { key = 'E', label = 'Use', onSelect = function(ctx) OpenAtm() end }, { key = 'G', label = 'Check balance', serverEvent = 'bank:server:balance' }, },})
-- Later: change it or remove itSI:update(id, { status = { ok = false, label = 'Out of service' } })SI:remove(id)Adding interactions
Section titled “Adding interactions”Every function returns an id (a number) to use with update and remove, or nil if the interaction was invalid. The reason is printed in the F8 console with the name of your resource.
| Export | Where the interaction goes |
|---|---|
addCoords(coords, def) |
A fixed point in the world (vector3) |
addLocalEntity(entity, def) |
An object, ped or vehicle created by this client (entity handle) |
addEntity(netId, def) |
A networked entity. It shows whenever the entity exists nearby, even if it is created later |
addModel(models, def) |
Every object, ped or vehicle of one or more models (name or hash) |
addGlobalVehicle(def) |
Every vehicle |
addGlobalPed(def) |
Every ped (NPC) |
addGlobalObject(def) |
Every object |
addGlobalPlayer(def) |
Every other player |
addMany(list) |
Many at once (see many at once) |
Without bones, an interaction on an entity covers the whole entity: the player can aim at any part of it, and the distance is measured to its body. With bones, it is that spot only. Both can live on the same vehicle: the trunk when aiming at the back, the vehicle menu anywhere else.
-- Every ped with this modelSI:addModel('a_m_y_business_01', { label = 'Businessman', icon = 'talk', reveal = 'look', options = { ... } })
-- The trunk of every vehicle, only when aiming at the backSI:addGlobalVehicle({ label = 'Trunk', icon = 'box', bones = 'boot', options = { { key = 'E', label = 'Open / close', onSelect = function(ctx) ToggleTrunk(ctx.entity) end } },})Interaction fields
Section titled “Interaction fields”| Field | Type | Description |
|---|---|---|
label |
text | Name shown on the label |
icon |
text | Icon of the point: a built-in icon or one registered with registerIcon |
options |
list | What the player can do (see options) |
distance |
number | Meters up to which the point is visible. Default: Config.Points.distance (2.0) |
offset |
vector3 |
Where the point goes, relative to the entity. Default: the center of its model |
bones |
text or list | Bone names of the entity, one point per bone ('boot', 'bonnet', 'door_dside_f'…) |
reveal |
text | When the point shows: 'always' (when close), 'look' (only when looked at) or 'key' (only while holding the reveal key, Alt) |
easy |
boolean | Gets focus just by looking towards it, without aiming at the point |
aimBoost |
number | Makes the point easier to focus: 2 counts as twice as close to the crosshair |
highlight |
text or false |
'outline', 'box', 'both' or false. The game outline is only drawn on objects |
grow |
boolean | The object grows a little while it is looked at |
moving |
boolean | The entity moves: its point is recalculated every frame. Peds and vehicles already do |
inVehicle |
boolean | Also shows from inside a vehicle (by default nothing shows from a vehicle) |
badge |
text | Short text next to the name: a price, an amount… |
status |
table | Tag next to the name: { ok = false, label = 'Out of service' }. Green or red |
disabled |
true or text |
The whole interaction is disabled. With a text, its keys turn red with that reason |
data |
any | Anything you want back in ctx.data |
Bones of vehicles
Section titled “Bones of vehicles”| Bone | Where the point goes |
|---|---|
door_dside_f, door_pside_f, door_dside_r, door_pside_r |
The handle of that door. It moves with the door when it opens |
boot |
The edge of the trunk. It rises with the lid when it opens |
bonnet |
The edge of the hood. It rises with the hood when it opens |
| Any other bone | The bone itself (petroltank, wheel_lf…) |
Options
Section titled “Options”Each option is a key on the label, or a line of the menu.
| Field | Type | Description |
|---|---|---|
label |
text | Text of the option (required) |
key |
text | One of Config.Keys ('E', 'G', 'H'). Without it, the option takes the first free key |
onSelect |
function | Runs when the key is pressed: function(ctx) ... end |
event |
text | Client event triggered with ctx |
serverEvent |
text | Server event triggered with ctx (without the entity handle, with netId if it is networked) |
canInteract |
function | function(ctx) return true end, or return false, 'reason' (see conditions) |
groups |
text or table | Jobs or gangs that can use it: 'police', { 'police', 'ambulance' } or { police = 2 } (minimum grade) |
items |
text or table | Items the player must carry: 'lockpick', { 'lockpick', 'gloves' } or { water = 2 } (amount) |
disabled |
true or text |
The option can’t be used. With a text, the key turns red with that reason when pressed |
showDisabled |
boolean | When a condition fails, the key is shown dimmed instead of hidden |
hold |
number | Milliseconds the key must be held. The key fills up while it is held |
confirm |
true or text |
The first press asks ('Sell it? Press again'), the second one does it |
options |
list | A submenu |
quantity |
true or table |
Choose an amount with the mouse wheel first: { min = 1, max = 10, step = 1, default = 1 }. The choice arrives in ctx.quantity |
price |
number | Price shown next to the option. With quantity, the total is shown while choosing |
description |
text | Help text under the option, in menus |
badge |
text | Short text next to the option |
image |
text or list | Picture of the option: a file of your resource, nui://, https:// or data:image/... |
itemImage |
text or list | Picture of an item from the server’s inventory, by item name |
An option needs onSelect, event, serverEvent or options, otherwise it does nothing.
Files of your own resource (icons and images) must be listed in files { } of your fxmanifest.lua.
onSelect, canInteract and event receive a ctx table:
| Field | Description |
|---|---|
id |
Id of the interaction |
key |
Key or menu entry that was used |
entity |
Entity handle (entities only) |
netId |
Network id, if the entity is networked |
coords |
Position of the entity or the point |
distance |
Distance from the player |
model |
Model of the entity |
bone |
Bone of the point, if it has one |
quantity |
Amount chosen, in options with quantity |
data |
The data given when adding the interaction |
A server event receives the same table without entity and distance:
RegisterNetEvent('shop:server:buy', function(ctx) local src = source -- ctx.netId, ctx.coords, ctx.quantity, ctx.data...end)When an interaction has more options than keys, or any option has options (submenu) or quantity, the label becomes a menu. The mouse wheel chooses, the first key (E) uses the chosen option, and the second key (G) or Backspace goes back. Keys given with key are ignored inside a menu.
SI:addModel('prop_vend_soda_01', { label = 'Vending machine', icon = 'product', options = { { label = 'Drinks', description = 'Cold drinks.', options = { { label = 'Water', price = 2, itemImage = 'water', quantity = { min = 1, max = 5 }, serverEvent = 'vending:server:buy', }, { label = 'Cola', price = 3, itemImage = 'kurkakola', serverEvent = 'vending:server:buy' }, }, }, { label = 'Kick it', hold = 1000, onSelect = function() return false, 'Nothing falls out' end }, { label = 'Smash the glass', confirm = 'Smash it? Press again', onSelect = function() SmashGlass() end }, },})Conditions
Section titled “Conditions”The fastest conditions are the ones SL-Interact checks by itself, without calling your script: distance, groups, items, disabled and inVehicle. Use them whenever you can.
canInteract is for everything else. It is only called when the player focuses the interaction, every 500 ms while it stays focused and when the key is pressed. Never for interactions that are nearby but not looked at.
canInteract returns |
What the player sees |
|---|---|
true |
The key shows normally |
false |
The key does not show and pressing it does nothing |
false, 'reason' |
The key does not show. If pressed, it turns red for a moment with the reason |
false, 'reason' with showDisabled = true |
The key is dimmed from the start and turns red with the reason when pressed |
For what only your script or the server knows (money, stock, permissions):
onSelectcan returnfalse, 'reason': the key turns red with the reason.- Client:
exports.sl_interact:deny(key, reason). - Server:
exports.sl_interact:deny(source, key, reason). If the player is no longer looking at anything, it shows as a normal notification.
-- Server: refuse after checking the moneyRegisterNetEvent('shop:server:buy', function(ctx) local src = source if not RemoveMoney(src, 50) then exports.sl_interact:deny(src, ctx.key, 'Not enough money') return endend)Changing and removing
Section titled “Changing and removing”| Export | Description |
|---|---|
update(id, fields) |
Changes any field of an interaction: label, options, badge, status, disabled… |
updateMany({ [id] = fields }) |
Several at once. Returns how many were changed |
remove(id) or remove({ ids }) |
Removes one or several |
removeMany(ids) |
Removes a list of ids |
removeLocalEntity(entity) |
Removes your interactions on that entity |
removeEntity(netId) |
Removes your interactions on that networked entity |
removeModel(models) |
Removes your interactions on those models |
-- The label of a door changes with its stateSI:update(doorId, { options = { { key = 'E', label = open and 'Close' or 'Open', onSelect = ToggleDoor },} })Many at once
Section titled “Many at once”Adding hundreds of interactions one by one crosses from your resource to SL-Interact hundreds of times. addMany does it in one call. Each entry says where it goes with one of these fields: coords, entity, netId, model / models, or global ('vehicle', 'ped', 'object' or 'player').
local ids = SI:addMany({ { coords = vec3(25.7, -1347.3, 29.5), label = 'Shelf 1', options = { ... } }, { coords = vec3(26.4, -1347.3, 29.5), label = 'Shelf 2', options = { ... } }, { model = 'prop_atm_01', label = 'ATM', options = { ... } },})-- ids comes back in the same order (false for the invalid ones)
SI:updateMany({ [ids[1]] = { badge = 'x3' }, [ids[2]] = { disabled = 'Empty' } })SI:removeMany(ids)Built-in icons: product, door, checkout, pay, sale, pc, box, lock, unlock, build, cash, person, car, hand, gear, talk.
Register your own with registerIcon(name, image) before using it:
-- A file of your resource (listed in files { } of your fxmanifest)SI:registerIcon('star', 'star.svg')-- Or: 'nui://resource/file.png', 'https://...', 'data:image/png;base64,...'If SL-Interact restarts
Section titled “If SL-Interact restarts”When SL-Interact stops, it forgets every interaction it was given. On a live server, add yours again when it starts:
AddEventHandler('onClientResourceStart', function(name) if name == 'sl_interact' then AddMyInteractions() endend)Restarting the whole server, or your resource after SL-Interact, needs nothing else.