2025-04-23 14:10:19 -05:00
Point = Point or Require ( "lib/points/client/points.lua" )
2025-08-27 06:13:02 -05:00
ClientEntity = {
2026-01-11 18:36:07 -06:00
All = {},
Invoked = {},
OnCreates = {}
2026-01-12 16:42:44 -06:00
}
2026-01-11 18:36:07 -06:00
----------------------------------------
---@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.
2026-01-12 16:42:44 -06:00
--- Set the entity and apply any relevant properties such as freezing and targets
--- @param id The entities id
--- @return nil
2026-01-11 18:36:07 -06:00
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
2025-08-23 22:53:49 -05:00
2026-01-11 18:36:07 -06:00
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
2025-04-23 14:10:19 -05:00
2026-01-12 16:42:44 -06:00
--- Spawns an entity
--- @param entityData EntityData
2025-04-23 14:10:19 -05:00
local function SpawnEntity ( entityData )
if entityData.spawned and DoesEntityExist ( entityData.spawned ) then return end -- Already spawned
2026-01-11 18:36:07 -06:00
if entityData.model then
2026-08-18 01:23:06 -07:00
local loaded , model = lib.requestModel ( entityData.model )
2025-09-03 08:57:39 -05:00
if not loaded then
print ( string.format ( "[ClientEntity] Failed to load model %s for entity %s" , entityData.model , entityData.id ))
return
end
2025-04-23 14:10:19 -05:00
2025-09-03 08:57:39 -05:00
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
2026-01-11 18:36:07 -06:00
entityData.spawned = entity
SetEntityProperties ( entityData.id )
SetModelAsNoLongerNeeded ( model )
2025-06-02 22:19:42 -05:00
end
2026-01-11 18:36:07 -06:00
if not entityData.OnSpawn then return end
pcall ( function (...)
return entityData.OnSpawn ( entityData )
end )
2025-04-23 14:10:19 -05:00
end
2026-01-12 16:42:44 -06:00
--- Removes an entity
--- @param entityData EntityData
2025-08-05 20:57:26 -05:00
local function RemoveEntity ( entityData )
2025-05-11 00:47:33 -05:00
entityData = entityData and entityData.args or entityData
if not entityData then return end
2025-08-05 20:57:26 -05:00
if entityData.OnRemove then
2025-08-12 20:20:49 -04:00
pcall ( function (...)
return entityData.OnRemove ( entityData )
end )
2025-08-05 20:57:26 -05:00
end
2025-04-23 14:10:19 -05:00
if entityData.spawned and DoesEntityExist ( entityData.spawned ) then
local entityHandle = entityData.spawned
2025-05-11 00:47:33 -05:00
entityData.spawned = nil
2025-08-12 20:20:49 -04:00
SetEntityAsMissionEntity ( entityHandle , true , true )
2025-04-23 14:10:19 -05:00
DeleteEntity ( entityHandle )
2025-08-12 20:20:49 -04:00
end
2025-04-23 14:10:19 -05:00
end
2026-01-11 18:36:07 -06:00
--- 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 )
2026-08-17 03:49:37 -07:00
entityData.id = entityData.id or lib.string . random ( 'Aa1A' , 8 ) -- Generate a random ID if not provided
2026-01-11 18:36:07 -06:00
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"
2025-08-23 22:53:49 -05:00
2026-01-11 18:36:07 -06:00
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
2025-08-23 22:53:49 -05:00
end
end
end
2025-09-03 01:04:04 -05:00
2025-08-23 22:53:49 -05:00
local entityPoint = Point.Register ( entityData.id , entityData.coords , entityData.spawnDistance or 50.0 , entityData , SpawnEntity , RemoveEntity , UpdateEntity )
2026-01-11 18:36:07 -06:00
ClientEntity.All [ entityData.id ] = entityPoint
2025-08-27 06:13:02 -05:00
ClientEntity.Invoked [ entityData.invoked ] = ClientEntity.Invoked [ entityData.invoked ] or {}
ClientEntity.Invoked [ entityData.invoked ][ entityData.id ] = entityData
2026-01-11 18:36:07 -06:00
for k , v in pairs ( entityPoint ) do
ClientEntity.SetKey ( entityData.id , k , v )
end
2025-08-23 22:53:49 -05:00
return entityPoint
2025-04-23 14:10:19 -05:00
end
2026-01-11 18:36:07 -06:00
--- 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
2025-06-30 15:18:01 -05:00
ClientEntity.Register = ClientEntity.Create
2026-01-11 18:36:07 -06:00
--- Registers multiple entities.
--- @param entities table<number, EntityData> List of ClientEntity definitions.
--- @return table<ClientEntityId, table> Map of ClientEntity id to registered entries.
2025-06-30 15:18:01 -05:00
function ClientEntity . CreateBulk ( entities )
2025-06-22 17:40:41 -05:00
local registeredEntities = {}
for _ , entityData in pairs ( entities ) do
2025-06-30 15:18:01 -05:00
local entity = ClientEntity.Create ( entityData )
registeredEntities [ entity.id ] = entity
2025-06-22 17:40:41 -05:00
end
return registeredEntities
end
2025-06-30 15:18:01 -05:00
-- Depricated use ClientEntity.CreateBulk instead
ClientEntity.RegisterBulk = ClientEntity.CreateBulk
2026-01-11 18:36:07 -06:00
--- 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.
2025-06-30 15:18:01 -05:00
function ClientEntity . Destroy ( id )
2026-01-11 18:36:07 -06:00
local entityData = ClientEntity.All [ id ]
2025-04-23 14:10:19 -05:00
if not entityData then return end
2025-06-02 22:19:42 -05:00
Point.Remove ( id )
2026-01-11 18:36:07 -06:00
RemoveEntity ( entityData )
ClientEntity.All [ id ] = nil
2025-04-23 14:10:19 -05:00
end
2025-06-30 15:18:01 -05:00
ClientEntity.Unregister = ClientEntity.Destroy
2026-01-11 18:36:07 -06:00
--- 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 )
2025-08-23 22:53:49 -05:00
local entityData = ClientEntity.Get ( id )
if not entityData then return print ( string.format ( "[ClientEntity] SetKey: Entity %s does not exist" , id )) end
2026-01-11 18:36:07 -06:00
2025-10-26 16:20:27 -04:00
local oldData = entityData [ key ]
2026-01-11 18:36:07 -06:00
local handlerKey = "On" .. key : sub ( 1 , 1 ): upper () .. key : sub ( 2 )
local handler = rawget ( entityData , handlerKey )
2026-01-11 20:37:42 -06:00
entityData [ key ] = value
2026-01-11 18:36:07 -06:00
if handler then
pcall ( function ()
handler ( entityData , key , value , oldData )
2025-10-26 16:20:27 -04:00
end )
end
2026-01-11 20:37:42 -06:00
2025-04-23 14:10:19 -05:00
end
2026-01-11 18:36:07 -06:00
--- 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.
2025-04-23 14:10:19 -05:00
function ClientEntity . Get ( id )
2026-01-11 18:36:07 -06:00
return ClientEntity.All [ id ]
2025-04-23 14:10:19 -05:00
end
2026-01-11 18:36:07 -06:00
--- Gets all registered ClientEntity entries keyed by ClientEntity id.
--- @return table<ClientEntityId, EntityData>
2025-04-23 14:10:19 -05:00
function ClientEntity . GetAll ()
2026-01-11 18:36:07 -06:00
return ClientEntity.All
2025-04-23 14:10:19 -05:00
end
2026-01-11 18:36:07 -06:00
--- 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
2026-01-12 16:42:44 -06:00
if not anim then return Bridge.Anim . Stop ( id ) end
2026-08-17 03:49:37 -07:00
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 )
2025-06-30 15:18:01 -05:00
end
2026-01-11 18:36:07 -06:00
--- 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 )
2025-08-12 19:23:15 -05:00
return
end
2026-01-11 18:36:07 -06:00
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 )
2025-08-12 19:23:15 -05:00
end
2026-01-11 18:36:07 -06:00
--- 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 )
2025-08-12 19:23:15 -05:00
end
2026-01-11 18:36:07 -06:00
--- Updates the stored coords for a ClientEntity and updates its proximity trigger.
--- @param id ClientEntityId ClientEntity id.
--- @param coords vector3 New coords.
2025-08-12 19:23:15 -05:00
function ClientEntity . UpdateCoords ( id , coords )
2026-01-11 18:36:07 -06:00
local entityData = ClientEntity.All [ id ]
2025-08-12 19:23:15 -05:00
if not entityData then
print ( string.format ( "[ClientEntity] UpdateCoords: Entity %s does not exist" , id ))
return
end
entityData.coords = coords
2026-01-11 18:36:07 -06:00
Point.UpdateCoords ( id , coords )
2026-01-11 20:37:42 -06:00
if entityData.spawned and DoesEntityExist ( entityData.spawned ) then
SetEntityCoords ( entityData.spawned , coords.x , coords.y , coords.z , false , false , false , true )
end
end
2026-01-12 16:42:44 -06:00
--- Updates the rotation for a registered entity
--- @param id ClientEntityId ClientEntity id.
--- @param rotation number|string
--- @return nil
2026-01-11 20:37:42 -06:00
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
2025-08-12 19:23:15 -05:00
end
2026-01-11 18:36:07 -06:00
--- 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.
2025-08-20 22:23:07 -05:00
function ClientEntity . ChangeModel ( id , model )
2026-01-11 18:36:07 -06:00
local entityData = ClientEntity.All [ id ]
2025-08-20 22:23:07 -05:00
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
2026-01-11 18:36:07 -06:00
--- 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
2025-08-20 22:23:07 -05:00
2025-04-23 14:10:19 -05:00
-- Network Event Handlers
RegisterNetEvent ( "community_bridge:client:CreateEntity" , function ( entityData )
2026-01-11 18:36:07 -06:00
local src = source
if src ~= 65535 then return end -- Server only
2025-06-30 15:18:01 -05:00
ClientEntity.Create ( entityData )
2025-04-23 14:10:19 -05:00
end )
2025-06-22 17:40:41 -05:00
RegisterNetEvent ( "community_bridge:client:CreateEntities" , function ( entities )
2026-01-11 18:36:07 -06:00
local src = source
if src ~= 65535 then return end -- Server only
2025-06-30 15:18:01 -05:00
ClientEntity.CreateBulk ( entities )
2025-06-22 17:40:41 -05:00
end )
2025-04-23 14:10:19 -05:00
RegisterNetEvent ( "community_bridge:client:DeleteEntity" , function ( id )
2026-01-11 18:36:07 -06:00
local src = source
if src ~= 65535 then return end -- Server only
ClientEntity.Destroy ( id )
2025-04-23 14:10:19 -05:00
end )
RegisterNetEvent ( "community_bridge:client:UpdateEntity" , function ( id , data )
2026-01-11 18:36:07 -06:00
local src = source
if src ~= 65535 then return end -- Server only
2025-08-27 05:47:44 -05:00
for k , v in pairs ( data ) do
2026-01-11 18:36:07 -06:00
ClientEntity.SetKey ( id , k , v )
2025-08-27 05:47:44 -05:00
end
end )
2025-04-23 14:10:19 -05:00
-- Resource Stop Cleanup
AddEventHandler ( 'onResourceStop' , function ( resourceName )
if resourceName == GetCurrentResourceName () then
2026-01-11 18:36:07 -06:00
for id , entityData in pairs ( ClientEntity.All ) do
Point.Remove ( id )
RemoveEntity ( entityData )
2025-04-23 14:10:19 -05:00
end
2026-01-11 18:36:07 -06:00
ClientEntity.All = {}
2025-08-27 06:13:02 -05:00
else
for id , entityData in pairs ( ClientEntity.Invoked [ resourceName ] or {}) do
2026-01-11 18:36:07 -06:00
Point.Remove ( id )
RemoveEntity ( entityData )
ClientEntity.All [ id ] = nil
2025-08-27 06:13:02 -05:00
end
2025-04-23 14:10:19 -05:00
end
end )
2025-08-30 19:12:56 -05:00
return ClientEntity