407 lines
16 KiB
Lua
407 lines
16 KiB
Lua
Point = Point or Require("lib/points/client/points.lua")
|
|
ClientEntity = {
|
|
All = {},
|
|
Invoked = {},
|
|
OnCreates = {}
|
|
}
|
|
----------------------------------------
|
|
|
|
---@alias ClientEntityId string|number
|
|
---@alias EntityHandle number
|
|
|
|
---@class ClientEntityAnim
|
|
---@field dict string
|
|
---@field name string
|
|
---@field blendIn number|nil
|
|
---@field blendOut number|nil
|
|
---@field duration number|nil
|
|
---@field flags number|nil
|
|
---@field playbackRate number|nil
|
|
---@field onComplete function|nil
|
|
|
|
---@class ClientEntityAttach
|
|
---@field target string|number Target reference. String may be a Bridge.Entity id; number may be a player server id; either may also be a FiveM entity handle.
|
|
---@field bone number|nil
|
|
---@field offset vector3|nil
|
|
---@field rotation vector3|nil
|
|
|
|
---@class EntityData
|
|
---@field id ClientEntityId|nil If omitted, one is generated.
|
|
---@field coords vector3
|
|
---@field entityType 'object'|'ped'|'vehicle'
|
|
---@field model string|number
|
|
---@field heading number|nil
|
|
---@field rotation vector3|nil
|
|
---@field spawnDistance number|nil
|
|
---@field freeze boolean|nil
|
|
---@field invincible boolean|nil
|
|
---@field targets table|nil Target options passed to Bridge.Target.AddLocalEntity.
|
|
---@field anim ClientEntityAnim|nil
|
|
---@field attach ClientEntityAttach|nil
|
|
---@field OnSpawn fun(entityData: EntityData)|nil
|
|
---@field OnRemove fun(entityData: EntityData)|nil
|
|
---@field spawned EntityHandle|nil FiveM entity handle created when the entry is in-range.
|
|
|
|
--- Set the entity and apply any relevant properties such as freezing and targets
|
|
--- @param id The entities id
|
|
--- @return nil
|
|
local function SetEntityProperties(id)
|
|
local entityData = ClientEntity.Get(id)
|
|
if not entityData then return end
|
|
local entity = entityData.spawned
|
|
if not entity or not DoesEntityExist(entity) then return end
|
|
|
|
if entityData.freeze == nil or entityData.freeze then
|
|
FreezeEntityPosition(entity, true)
|
|
end
|
|
if entityData.invincible then
|
|
SetEntityInvincible(entity, entityData.invincible)
|
|
end
|
|
if entityData.targets then
|
|
ClientEntity.SetTargets(id, entityData.targets)
|
|
end
|
|
if entityData.anim then
|
|
ClientEntity.SetAnim(id, entityData.anim)
|
|
end
|
|
if entityData.attach then
|
|
ClientEntity.SetAttach(id, entityData.attach)
|
|
end
|
|
end
|
|
|
|
--- Spawns an entity
|
|
--- @param entityData EntityData
|
|
local function SpawnEntity(entityData)
|
|
if entityData.spawned and DoesEntityExist(entityData.spawned) then return end -- Already spawned
|
|
if entityData.model then
|
|
local loaded, model = lib.requestModel(entityData.model)
|
|
if not loaded then
|
|
print(string.format("[ClientEntity] Failed to load model %s for entity %s", entityData.model, entityData.id))
|
|
return
|
|
end
|
|
|
|
local entity = nil
|
|
local coords = entityData.coords
|
|
local rotation = entityData.rotation or vector3(0.0, 0.0, entityData.heading or 0.0) -- Default rotation if not provided
|
|
|
|
if entityData.entityType == 'object' then
|
|
entity = CreateObject(model, coords.x, coords.y, coords.z, false, false, false)
|
|
SetEntityRotation(entity, rotation.x, rotation.y, rotation.z, 2, true)
|
|
elseif entityData.entityType == 'ped' then
|
|
entity = CreatePed(4, model, coords.x, coords.y, coords.z, type(rotation) == 'number' and rotation or rotation.z, false, false)
|
|
elseif entityData.entityType == 'vehicle' then
|
|
entity = CreateVehicle(model, coords.x, coords.y, coords.z, type(rotation) == 'number' and rotation or rotation.z, false, false)
|
|
else
|
|
print(string.format("[ClientEntity] Unknown entity type '%s' for entity %s", entityData.entityType, entityData.id))
|
|
end
|
|
entityData.spawned = entity
|
|
SetEntityProperties(entityData.id)
|
|
SetModelAsNoLongerNeeded(model)
|
|
end
|
|
if not entityData.OnSpawn then return end
|
|
pcall(function (...)
|
|
return entityData.OnSpawn(entityData)
|
|
end)
|
|
end
|
|
|
|
--- Removes an entity
|
|
--- @param entityData EntityData
|
|
local function RemoveEntity(entityData)
|
|
entityData = entityData and entityData.args or entityData
|
|
if not entityData then return end
|
|
if entityData.OnRemove then
|
|
pcall(function (...)
|
|
return entityData.OnRemove(entityData)
|
|
end)
|
|
end
|
|
if entityData.spawned and DoesEntityExist(entityData.spawned) then
|
|
local entityHandle = entityData.spawned
|
|
entityData.spawned = nil
|
|
SetEntityAsMissionEntity(entityHandle, true, true)
|
|
DeleteEntity(entityHandle)
|
|
end
|
|
end
|
|
|
|
--- Registers a ClientEntity definition and a proximity trigger that spawns/despawns it.
|
|
---
|
|
--- Note: When in-range, the spawned FiveM entity handle is stored on `entityData.spawned`.
|
|
--- @param entityData EntityData
|
|
--- @return table Registered point/entity entry (the value stored in ClientEntity.All[entityData.id]).
|
|
function ClientEntity.Create(entityData)
|
|
entityData.id = entityData.id or lib.string.random('Aa1A', 8) -- Generate a random ID if not provided
|
|
if ClientEntity.All[entityData.id] then return ClientEntity.All[entityData.id] end
|
|
entityData.rotation = entityData.rotation or vector3(0.0, 0.0, entityData.heading or 0.0)
|
|
entityData.invoked = entityData.invoked or GetInvokingResource() or "community_bridge"
|
|
|
|
for k, v in pairs(ClientEntity.OnCreates) do
|
|
if entityData[k] ~= nil then
|
|
local success, results = pcall(function()
|
|
return v(entityData)
|
|
end)
|
|
if success then
|
|
entityData = results or entityData
|
|
end
|
|
end
|
|
end
|
|
|
|
local entityPoint = Point.Register(entityData.id, entityData.coords, entityData.spawnDistance or 50.0, entityData, SpawnEntity, RemoveEntity, UpdateEntity)
|
|
ClientEntity.All[entityData.id] = entityPoint
|
|
ClientEntity.Invoked[entityData.invoked] = ClientEntity.Invoked[entityData.invoked] or {}
|
|
ClientEntity.Invoked[entityData.invoked][entityData.id] = entityData
|
|
|
|
for k, v in pairs(entityPoint) do
|
|
ClientEntity.SetKey(entityData.id, k, v)
|
|
end
|
|
return entityPoint
|
|
end
|
|
|
|
--- Gets the entity that was spawned or nil if player is not in range
|
|
---@param id ClientEntityId
|
|
---@return EntityHandle|nil
|
|
function ClientEntity.GetEntity(id)
|
|
local entityData = ClientEntity.Get(id)
|
|
if not entityData then return nil end
|
|
return entityData.spawned
|
|
end
|
|
---Deprecated use ClientEntity.Create instead
|
|
ClientEntity.Register = ClientEntity.Create
|
|
|
|
--- Registers multiple entities.
|
|
--- @param entities table<number, EntityData> List of ClientEntity definitions.
|
|
--- @return table<ClientEntityId, table> Map of ClientEntity id to registered entries.
|
|
function ClientEntity.CreateBulk(entities)
|
|
local registeredEntities = {}
|
|
for _, entityData in pairs(entities) do
|
|
local entity = ClientEntity.Create(entityData)
|
|
registeredEntities[entity.id] = entity
|
|
end
|
|
return registeredEntities
|
|
end
|
|
-- Depricated use ClientEntity.CreateBulk instead
|
|
ClientEntity.RegisterBulk = ClientEntity.CreateBulk
|
|
|
|
--- Unregisters a ClientEntity registration and deletes its spawned FiveM entity (if any).
|
|
--- @param id ClientEntityId ClientEntity id (key used in ClientEntity.All / Point system), not a FiveM entity handle.
|
|
function ClientEntity.Destroy(id)
|
|
local entityData = ClientEntity.All[id]
|
|
if not entityData then return end
|
|
Point.Remove(id)
|
|
RemoveEntity(entityData)
|
|
ClientEntity.All[id] = nil
|
|
end
|
|
ClientEntity.Unregister = ClientEntity.Destroy
|
|
|
|
--- Sets a property on a registered ClientEntity entry.
|
|
--- If the entry defines an On<Key> handler (example: key='coords' -> OnCoords), it will be called.
|
|
--- @param id ClientEntityId ClientEntity id (key used in ClientEntity.All), not a FiveM entity handle.
|
|
--- @param key string Field name on the stored entity data table.
|
|
--- @param value any Value to set.
|
|
--- @return nil
|
|
function ClientEntity.SetKey(id, key, value)
|
|
local entityData = ClientEntity.Get(id)
|
|
if not entityData then return print(string.format("[ClientEntity] SetKey: Entity %s does not exist", id)) end
|
|
|
|
local oldData = entityData[key]
|
|
local handlerKey = "On" .. key:sub(1, 1):upper() .. key:sub(2)
|
|
local handler = rawget(entityData, handlerKey)
|
|
entityData[key] = value
|
|
if handler then
|
|
pcall(function()
|
|
handler(entityData, key, value, oldData)
|
|
end)
|
|
end
|
|
|
|
end
|
|
|
|
--- Gets a registered ClientEntity entry.
|
|
--- @param id ClientEntityId ClientEntity id (key used in ClientEntity.All), not a FiveM entity handle.
|
|
--- @return EntityData|nil The registered entry; when spawned, entityData.spawned is the FiveM entity handle.
|
|
function ClientEntity.Get(id)
|
|
return ClientEntity.All[id]
|
|
end
|
|
|
|
--- Gets all registered ClientEntity entries keyed by ClientEntity id.
|
|
--- @return table<ClientEntityId, EntityData>
|
|
function ClientEntity.GetAll()
|
|
return ClientEntity.All
|
|
end
|
|
|
|
--- Plays or stops an animation on the spawned FiveM entity for a ClientEntity.
|
|
--- @param id ClientEntityId ClientEntity id.
|
|
--- @param anim ClientEntityAnim|nil Animation data; pass nil to stop.
|
|
function ClientEntity.SetAnim(id, anim)
|
|
local entityData = ClientEntity.Get(id)
|
|
if not entityData then return print(string.format("[ClientEntity] SetAnim: Entity %s does not exist", id)) end
|
|
local entity = entityData.spawned
|
|
if not entity or not DoesEntityExist(entity) then return end
|
|
if not anim then return Bridge.Anim.Stop(id) end
|
|
lib.playAnim(entity, anim.dict, anim.name, anim.blendIn or 8.0, anim.blendOut or -8.0, anim.duration, anim.flags, anim.playbackRate or 0.0)
|
|
|
|
end
|
|
|
|
--- Attaches the spawned FiveM entity for a ClientEntity to a target, or detaches when nil.
|
|
--- @param id ClientEntityId ClientEntity id.
|
|
--- @param attachData ClientEntityAttach|nil Attachment data; pass nil to detach.
|
|
function ClientEntity.SetAttach(id, attachData)
|
|
local entityData = ClientEntity.Get(id)
|
|
if not entityData then return print(string.format("[ClientEntity] Attach: Entity %s does not exist", id)) end
|
|
local entity = entityData.spawned
|
|
if not attachData then
|
|
if not entityData.spawned or not IsEntityAttached(entityData.spawned) then return end
|
|
DetachEntity(entityData.spawned, true, true)
|
|
return
|
|
end
|
|
local targetEntity = attachData.target
|
|
if type(targetEntity) == "string" then
|
|
targetEntity = Bridge.Entity.Get(targetEntity)?.spawned or targetEntity
|
|
else
|
|
local player = GetPlayerFromServerId(targetEntity)
|
|
if player and player ~= -1 then
|
|
targetEntity = GetPlayerPed(player)
|
|
end
|
|
end
|
|
targetEntity = tonumber(targetEntity)
|
|
if not targetEntity then return end
|
|
local bone = attachData.bone or 0
|
|
local offset = attachData.offset or vector3(0.0, 0.0, 0.0)
|
|
local rotation = attachData.rotation or vector3(0.0, 0.0, 0.0)
|
|
AttachEntityToEntity(entity, targetEntity, GetPedBoneIndex(targetEntity, bone), offset.x, offset.y, offset.z, rotation.x, rotation.y, rotation.z, true, true, false, true, 1, true)
|
|
end
|
|
|
|
--- Adds target options to the spawned FiveM entity for a ClientEntity.
|
|
--- @param id ClientEntityId ClientEntity id.
|
|
--- @param options table Target config passed to Bridge.Target.AddLocalEntity.
|
|
function ClientEntity.SetTargets(id, options)
|
|
local entityData = ClientEntity.Get(id)
|
|
if not entityData then return end
|
|
Bridge.Target.RemoveLocalEntity(entityData.spawned)
|
|
entityData.targets = options
|
|
if not options then return end
|
|
Bridge.Target.AddLocalEntity(entityData.spawned, options)
|
|
end
|
|
|
|
--- Updates the stored coords for a ClientEntity and updates its proximity trigger.
|
|
--- @param id ClientEntityId ClientEntity id.
|
|
--- @param coords vector3 New coords.
|
|
function ClientEntity.UpdateCoords(id, coords)
|
|
local entityData = ClientEntity.All[id]
|
|
if not entityData then
|
|
print(string.format("[ClientEntity] UpdateCoords: Entity %s does not exist", id))
|
|
return
|
|
end
|
|
entityData.coords = coords
|
|
Point.UpdateCoords(id, coords)
|
|
if entityData.spawned and DoesEntityExist(entityData.spawned) then
|
|
SetEntityCoords(entityData.spawned, coords.x, coords.y, coords.z, false, false, false, true)
|
|
end
|
|
end
|
|
|
|
--- Updates the rotation for a registered entity
|
|
--- @param id ClientEntityId ClientEntity id.
|
|
--- @param rotation number|string
|
|
--- @return nil
|
|
function ClientEntity.UpdateRotation(id, rotation)
|
|
local entityData = ClientEntity.All[id]
|
|
if not entityData then
|
|
print(string.format("[ClientEntity] UpdateRotation: Entity %s does not exist", id))
|
|
return
|
|
end
|
|
if type(rotation) == "number" or type(rotation) == "string" then
|
|
rotation = vector3(0.0, 0.0, tonumber(rotation))
|
|
end
|
|
entityData.rotation = rotation
|
|
if entityData.spawned and DoesEntityExist(entityData.spawned) then
|
|
SetEntityRotation(entityData.spawned, rotation.x, rotation.y, rotation.z, 2, true)
|
|
end
|
|
end
|
|
|
|
--- Changes the model used by a ClientEntity and respawns it if currently spawned.
|
|
--- @param id ClientEntityId ClientEntity id.
|
|
--- @param model string|number Model name or hash.
|
|
function ClientEntity.ChangeModel(id, model)
|
|
local entityData = ClientEntity.All[id]
|
|
if not entityData then return print(string.format("[ClientEntity] ChangeModel: Entity %s does not exist", id)) end
|
|
|
|
entityData.model = model
|
|
if not entityData.spawned or not DoesEntityExist(entityData.spawned) then return end
|
|
|
|
RemoveEntity(entityData)
|
|
SpawnEntity(entityData)
|
|
end
|
|
|
|
--- Hooks onto entity creation by checking for a property on the entityData. Includes ServerEntity creations.
|
|
--- func = function(entityData) return modifiedEntityData end
|
|
--- @param property any property to look for when creating the entity
|
|
--- @param func any
|
|
function ClientEntity.SetOnCreate(property, func)
|
|
ClientEntity.OnCreates[property] = func
|
|
end
|
|
|
|
--- Sets the OnSpawn callback for a ClientEntity.
|
|
--- @param id ClientEntityId ClientEntity id.
|
|
--- @param func fun(entityData: EntityData) Called when in-range spawn happens; entityData.spawned is the FiveM entity handle.
|
|
function ClientEntity.SetOnSpawn(id, func)
|
|
local entityData = ClientEntity.All[id]
|
|
if not entityData then
|
|
print(string.format("[ClientEntity] SetOnSpawn: Entity %s does not exist", id))
|
|
return
|
|
end
|
|
entityData.OnSpawn = func
|
|
end
|
|
|
|
--- Sets the OnRemove callback for a ClientEntity.
|
|
--- @param id ClientEntityId ClientEntity id.
|
|
--- @param func fun(entityData: EntityData) Called when out-of-range removal happens (and before deleting the spawned entity).
|
|
function ClientEntity.SetOnRemove(id, func)
|
|
local entityData = ClientEntity.All[id]
|
|
if not entityData then
|
|
print(string.format("[ClientEntity] SetOnRemove: Entity %s does not exist", id))
|
|
return
|
|
end
|
|
entityData.OnRemove = func
|
|
end
|
|
|
|
-- Network Event Handlers
|
|
RegisterNetEvent("community_bridge:client:CreateEntity", function(entityData)
|
|
local src = source
|
|
if src ~= 65535 then return end -- Server only
|
|
ClientEntity.Create(entityData)
|
|
end)
|
|
|
|
RegisterNetEvent("community_bridge:client:CreateEntities", function(entities)
|
|
local src = source
|
|
if src ~= 65535 then return end -- Server only
|
|
ClientEntity.CreateBulk(entities)
|
|
end)
|
|
|
|
RegisterNetEvent("community_bridge:client:DeleteEntity", function(id)
|
|
local src = source
|
|
if src ~= 65535 then return end -- Server only
|
|
ClientEntity.Destroy(id)
|
|
end)
|
|
|
|
RegisterNetEvent("community_bridge:client:UpdateEntity", function(id, data)
|
|
local src = source
|
|
if src ~= 65535 then return end -- Server only
|
|
for k, v in pairs(data) do
|
|
ClientEntity.SetKey(id, k, v)
|
|
end
|
|
end)
|
|
|
|
-- Resource Stop Cleanup
|
|
AddEventHandler('onResourceStop', function(resourceName)
|
|
if resourceName == GetCurrentResourceName() then
|
|
for id, entityData in pairs(ClientEntity.All) do
|
|
Point.Remove(id)
|
|
RemoveEntity(entityData)
|
|
end
|
|
ClientEntity.All = {}
|
|
else
|
|
for id, entityData in pairs(ClientEntity.Invoked[resourceName] or {}) do
|
|
Point.Remove(id)
|
|
RemoveEntity(entityData)
|
|
ClientEntity.All[id] = nil
|
|
end
|
|
end
|
|
end)
|
|
|
|
return ClientEntity |