Files
2026-09-16 16:51:51 +05:45

588 lines
19 KiB
Lua

local log = require 'shared.log'
local utils = require 'client.utils'
local settings = require 'shared.settings'
local interactions, filteredInteractions = {}, {}
local table_sort = table.sort
-- CACHE.
local MODELS = {}
local ENTITIES = {}
local ENTITY_BONES = {}
local NETWORKED_ENTITIES = {}
local api = {}
AddEventHandler('interactions:groupsChanged', function(newgroups)
-- Use this event handler to loop through all current interactions and remove any that are not in the new groups that way we limit the amount of iterations needed
end)
local function checkParams(entity, options)
if not entity then
log:error('Entity is required to add an interaction')
return
else
if type(entity) ~= 'number' then
log:error('Entity must be a number')
return
end
if not DoesEntityExist(entity) then
log:error('Entity %s does not exist', entity)
return
end
end
if not options then
log:error('Options are required to add an interaction')
return
end
return true
end
--#TODO: Add a way to filter interactions based on the players group
local function filterInteractions()
local myGroups = 'police'
local newInteractions = {}
local amount = 0
for k ,v in pairs(interactions) do
amount += 1
newInteractions[amount] = v
end
filteredInteractions = newInteractions
end
---@param model number|string : The model to add the interaction to
---@param options table : { label, canInteract, action, event, serverEvent, args }
---@param data table : { distance, interactDst, resource }
local function addModel(model, options, data)
if type(model) ~= "number" then
model = joaat(model)
end
if not IsModelValid(model) then
log:error('Model %s is not valid', model)
return
end
if not MODELS[model] then
log:debug('Adding model %s to interactions', model)
MODELS[model] = {
model = model,
offset = data.offset,
options = options,
distance = data.distance,
interactDst = data.interactDst,
resource = data.resource,
}
else
log:debug('Updating model %s in interactions', model)
for _, option in pairs(options) do
MODELS[model].options[#MODELS[model].options + 1] = option
end
-- Update the distance and interactDst if the new data is greater
if data.distance > MODELS[model].distance then
MODELS[model].distance = data.distance
end
if data.interactDst > MODELS[model].interactDst then
MODELS[model].interactDst = data.interactDst
end
-- Update the offset if there is new data
if data.offset then
MODELS[model].offset = data.offset
end
end
end
---@param data table : { name, coords, options, distance, interactDst, groups }
---@return number : The id of the interaction
-- Add an interaction point at a set of coords
function api.addInteraction(data)
if not data.coords then
log:error('Coords are required to add an interaction')
return
end
if not data.options then
log:error('Options are required to add an interaction')
return
end
local id = #interactions + 1
interactions[id] = {
id = id,
name = data.name or 'interaction:'..id,
coords = data.coords,
options = data.options or {},
distance = data.distance or 8.0,
interactDst = data.interactDst or 1.0,
groups = data.groups or nil,
resource = GetInvokingResource()
}
filterInteractions()
return id
end exports('AddInteraction', api.addInteraction)
---@param data table : { name, entity, options, distance, interactDst, groups }
---@return number : The id of the interaction
-- Add an interaction point on a local (client side) entity
function api.addLocalEntityInteraction(data)
local entity = data.entity
if not checkParams(entity, data.options) then
if ENTITIES[entity] then
ENTITIES[entity] = nil
end
return
end
local id = #interactions + 1
-- If then entity not registered yet, add it
if not ENTITIES[entity] then
log:debug('Adding entity %s to interactions', entity)
interactions[id] = {
id = id,
name = data.name or 'interaction:'..id,
entity = entity,
options = data.options or {},
distance = data.distance or 8.0,
interactDst = data.interactDst or 1.0,
offset = data.offset or vec(0.0, 0.0, 0.0),
groups = data.groups or nil,
resource = GetInvokingResource()
}
ENTITIES[entity] = interactions[id]
filterInteractions()
return id
end
-- If the entity is already registered, update it
log:debug('Updating entity %s in interactions', entity)
for index, option in pairs(data.options) do
if option.name and ENTITIES[entity].options[index]?.name == option.name then
log:debug('Option with name: ( %s ) already exists, updating', option.name)
ENTITIES[entity].options[index] = option
else
ENTITIES[entity].options[#ENTITIES[entity].options + 1] = option
end
end
-- Update the distance and interactDst if the new data is greater
if data.distance > ENTITIES[entity].distance then
ENTITIES[entity].distance = data.distance
end
if data.interactDst > ENTITIES[entity].interactDst then
ENTITIES[entity].interactDst = data.interactDst
end
-- Update the offset if there is new data
if data.offset then
ENTITIES[entity].offset = data.offset
end
interactions[id] = {
id = id,
entity = entity,
options = ENTITIES[entity].options,
distance = ENTITIES[entity].distance,
interactDst = ENTITIES[entity].interactDst,
offset = ENTITIES[entity].offset,
resource = GetInvokingResource()
}
filterInteractions()
return id
end exports('AddLocalEntityInteraction', api.addLocalEntityInteraction)
---@param data table : { name, netId, options, distance, interactDst, groups }
---@return number : The id of the interaction
-- Add an interaction point on a networked entity
function api.addEntityInteraction(data)
local netId = data.netId
-- If the netId does not exist, we assume it is an entity
local entity
if type(netId) == 'number' and NetworkDoesNetworkIdExist(netId) then
entity = utils.getEntity(netId)
elseif type(netId) == 'number' then
entity = netId
end
if not checkParams(entity, data.options) then
if NETWORKED_ENTITIES[netId] then
NETWORKED_ENTITIES[netId] = nil
end
return
end
-- If the entity is not networked, add it as a local entity
if not NetworkGetEntityIsNetworked(entity) then
log:debug('Entity %s is not networked, adding as a local entity', entity)
data.entity = entity
return api.addLocalEntityInteraction(data)
end
netId = utils.getNet(entity)
local id = #interactions + 1
-- If then netId not registered yet, add it
if not NETWORKED_ENTITIES[netId] then
log:debug('Adding networkID %s to interactions', netId)
interactions[id] = {
id = id,
name = data.name or 'interaction:'..id,
entity = entity,
options = data.options or {},
distance = data.distance or 10.0,
interactDst = data.interactDst or 1.0,
offset = data.offset or vec(0.0, 0.0, 0.0),
groups = data.groups or nil,
resource = GetInvokingResource()
}
NETWORKED_ENTITIES[netId] = interactions[id]
filterInteractions()
return id
end
-- If the networkID is already registered, update it
log:debug('Updating networkID %s in interactions', netId)
for index, option in pairs(data.options) do
if option.name and NETWORKED_ENTITIES[netId].options[index]?.name == option.name then
log:debug('Option with name: ( %s ) already exists, updating', option.name)
NETWORKED_ENTITIES[netId].options[index] = option
else
NETWORKED_ENTITIES[netId].options[#NETWORKED_ENTITIES[netId].options + 1] = option
end
end
-- Update the distance and interactDst if the new data is greater
if data.distance > NETWORKED_ENTITIES[netId].distance then
NETWORKED_ENTITIES[netId].distance = data.distance
end
if data.interactDst > NETWORKED_ENTITIES[netId].interactDst then
NETWORKED_ENTITIES[netId].interactDst = data.interactDst
end
-- Update the offset if there is new data
if data.offset then
NETWORKED_ENTITIES[netId].offset = data.offset
end
interactions[id] = {
id = id,
entity = entity,
options = NETWORKED_ENTITIES[netId].options,
distance = NETWORKED_ENTITIES[netId].distance,
interactDst = NETWORKED_ENTITIES[netId].interactDst,
offset = NETWORKED_ENTITIES[netId].offset,
resource = GetInvokingResource()
}
filterInteractions()
return id
end exports('AddEntityInteraction', api.addEntityInteraction)
---@param data table : { name, entity[number|string], bone[string], options, distance, interactDst, groups }
---@return number : The id of the interaction
-- Add an interaction point on a networked entity's bone
function api.addEntityBoneInteraction(data)
if not data.entity then
log:error('Entity is required to add an interaction')
return
end
if not data.bone then
log:error('Bone is required to add an interaction')
return
end
if not data.options then
log:error('Options are required to add an interaction')
return
end
-- temp workaround until table refactoring.
local key = string.format('%s:%s', data.entity, data.bone)
if not ENTITY_BONES[key] then
log:debug('Added new entity bone interaction: %s', key)
ENTITY_BONES[key] = {
entity = data.entity,
bone = data.bone,
distance = data.distance or 10.0,
interactDst = data.interactDst or 1.0,
offset = data.offset or vec(0.0, 0.0, 0.0),
options = data.options,
groups = data.groups or nil,
}
else
log:debug('Updating %s in bone interactions', key)
for index, option in pairs(data.options) do
if option.name and ENTITY_BONES[key].options[index]?.name == option.name then
log:debug('Option with name: ( %s ) already exists, updating', option.name)
ENTITY_BONES[key].options[index] = option
else
ENTITY_BONES[key].options[#ENTITY_BONES[key].options + 1] = option
end
end
-- Update the distance and interactDst if the new data is greater
if data.distance > ENTITY_BONES[key].distance then
ENTITY_BONES[key].distance = data.distance
end
if data.interactDst > ENTITY_BONES[key].interactDst then
ENTITY_BONES[key].interactDst = data.interactDst
end
-- Update the offset if there is new data
if data.offset then
ENTITY_BONES[key].offset = data.offset
end
log:debug('Updated entity bone interaction: %s', key)
ENTITY_BONES[key] = {
entity = data.entity,
bone = data.bone,
options = ENTITY_BONES[key].options,
distance = ENTITY_BONES[key].distance,
interactDst = ENTITY_BONES[key].interactDst,
offset = ENTITY_BONES[key].offset,
resource = GetInvokingResource()
}
end
filterInteractions()
return id
end exports('AddEntityBoneInteraction', api.addEntityBoneInteraction)
---@param data table : { name, modelData table : { model[string], offset[vec3] }, options, distance, interactDst, groups }
-- Add interaction(s) to a list of models
function api.addModelInteraction(data)
data.distance = data.distance or 8.0
data.interactDst = data.interactDst or 1.0
for i = 1, #data.modelData do
local modelData = data.modelData[i]
if IsModelValid(modelData.model) then
local min, max = GetModelDimensions(modelData.model)
local size = (max - min)
data.interactDst += (size.x / 8)
data.distance += (size.x / 4)
data.resource = GetInvokingResource()
addModel(modelData.model, data.options, { offset = modelData.offset, distance = data.distance, interactDst = data.interactDst, resource = data.resource })
end
end
end exports('AddModelInteraction', api.addModelInteraction)
---@param id number : The id of the interaction to remove
-- Remove an interaction point by id.
function api.removeInteraction(id)
interactions[id] = nil
log:debug('Removed interaction %s', id)
filterInteractions()
end
exports('RemoveInteraction', api.removeInteraction)
---@param entity number : The entity to remove the interaction from
-- Remove an interaction point by entity.
function api.removeInteractionByEntity(entity)
for i = #interactions, 1, -1 do
local interaction = interactions[i]
if interaction.entity == entity then
api.removeInteraction(i)
end
end
end exports('RemoveInteractionByEntity', api.removeInteractionByEntity)
---@param id number : The id of the interaction to remove the option from
---@param name? string : The name of the option to remove
-- Remove an option from an interaction point by id.
function api.removeInteractionOption(id, name)
if not interactions[id] then
log:error('Interaction with id: ( %s ) does not exist', id)
return
end
if not name then
api.removeInteraction(id)
return
end
local options = interactions[id].options
if not options then
log:error('Interaction with id: ( %s ) does not have any options', id)
api.removeInteraction(id)
return
end
for i = #options, 1, -1 do
local option = options[i]
if option.name == name then
options[i] = nil
log:debug('Removed option %s from interaction %s', name, id)
end
end
end exports('RemoveInteractionOption', api.removeInteractionOption)
---@param id number : The id of the interaction to update
---@param options table : The new options to update the interaction with
-- Update an interaction point by id.
function api.updateInteraction(id, options)
if not options then
log:error('Options are required to update an interaction')
return
end
if not interactions[id] then
log:error('Interaction with id: ( %s ) does not exist', id)
return
end
if interactions[id] then
interactions[id].options = options
filterInteractions()
end
end exports('UpdateInteraction', api.updateInteraction)
function api.getNearbyInteractions()
local options = {}
local amount = 0
local playercoords = GetEntityCoords(cache.ped)
-- Temp loop : these checks need to be broken out into their own threads.
local nearbyVehicles = lib.getNearbyVehicles(playercoords, settings.nearbyVehicleDistance, false)
for i = 1, #nearbyVehicles do
local vehicle = nearbyVehicles[i].vehicle
local vehicleCoords = nearbyVehicles[i].coords
if settings.vehicleBoneDefaults.enabled then
for bone, data in pairs(settings.vehicleBoneDefaults.bones) do
local key = string.format('%s:%s', vehicle, bone)
if not ENTITY_BONES[key] then
api.addEntityBoneInteraction({
entity = vehicle,
bone = bone,
options = data.options,
distance = data.distance,
interactDst = data.interactDst,
offset = data.offset,
})
end
end
end
end
for _, interaction in pairs(ENTITY_BONES) do
local distance = #(utils.getCoordsFromInteract(interaction) - playercoords)
if distance <= interaction.distance then
amount += 1
interaction.curDist = distance
options[amount] = interaction
end
end
local nearbyObjects = lib.getNearbyObjects(playercoords, settings.nearbyObjectDistance)
for i = 1, #nearbyObjects do
local nearby = nearbyObjects[i]
local hash = GetEntityModel(nearby.object)
if MODELS[hash] then
local interaction = lib.table.deepclone(MODELS[hash])
-- put the functions to original references
for k, v in pairs(interaction) do
if type(v) == "table" then
for id, item in pairs(v) do
if item.action then
item.action = MODELS[hash].options[id].action
end
if MODELS[hash].options[id].canInteract then
item.canInteract = MODELS[hash].options[id].canInteract
end
if item.canInteract then
local canInteract = false
pcall(function()
canInteract = item.canInteract(nearby.object, nearby.coords, item.args)
end)
v[id] = canInteract and item or nil
end
end
end
end
local distance = #(nearby.coords - playercoords)
interaction.entity = nearby.object
if distance <= interaction.distance then
amount += 1
interaction.curDist = distance
options[amount] = interaction
end
end
end
local amountOfInteractions = #filteredInteractions
if amountOfInteractions > 0 then
for i = 1, amountOfInteractions do
local interaction = filteredInteractions[i]
local coords = interaction.coords or utils.getCoordsFromInteract(interaction)
local distance = #(coords - playercoords)
if distance <= interaction.distance then
amount += 1
interaction.curDist = distance
options[amount] = interaction
end
end
end
for _, v in pairs(options) do
for i = 1, #v.options, 1 do
if not v.options[i] then
table.remove(v.options, i)
end
end
end
if amount > 1 then
table_sort(options, function(a, b)
return a.curDist < b.curDist
end)
end
return options
end
function api.disable(state)
LocalPlayer.state:set('interactionsDisabled', state, true)
end exports('Disable', api.disable)
AddEventHandler('onClientResourceStop', function(resource)
for i = #interactions, 1, -1 do
local interaction = interactions[i]
if interaction.resource == resource then
api.removeInteraction(interaction.id)
end
end
end)
return api