Files

407 lines
16 KiB
Lua
Raw Permalink Normal View History

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
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
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
2025-04-23 14:10:19 -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"
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
end
end
end
2025-09-03 01:04:04 -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
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)
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
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)
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