Files
community_bridge/lib/shared/rebound_entities.lua
T
gononono64 84480f8939 dkm
Ripped a part utility folder and shoved it into a lib. Changed a lot of Utility prefixes to the modules name. This contains breaking changes. Scripts that use 0.2.0 will need to update to match the new format
2025-02-05 17:42:50 -06:00

470 lines
15 KiB
Lua

-- -- -- -- -- -- -- -- -- --
-- ▄▀▀ █▄█ ▄▀▄ █▀▄ ██▀ █▀▄ --
-- ▄█▀ █ █ █▀█ █▀▄ █▄▄ █▄▀ --
-- -- -- -- -- -- -- -- -- --
local Entities = {}
local isServer = IsDuplicityVersion()
local registeredFunctions = {}
ReboundEntities = {}
--- Global Add function which fire when an entity is registered
--- @param func function = function(entityData: table)
--- @return number = ID
function ReboundEntities.AddFunction(func)
assert(func, "Func is nil")
table.insert(registeredFunctions, func)
return #registeredFunctions
end
--- Remove a registered function
--- @param id number
function ReboundEntities.RemoveFunction(id)
assert(id, "ID is nil")
assert(registeredFunctions[id], "ID not found")
registeredFunctions[id] = false
end
--- Internal: Fire all registered functions
--- @vararg any
local function FireFunctions(...)
for k, v in pairs(registeredFunctions) do
if v then
v(...)
end
end
end
--- Convert a table to an entityData object
--- @param entityData table = {model: string, position: vector3, id: string || nil}
--- @return table = {model: string, position: vector3, id: string, entity: number, isServer: boolean || nil}
function ReboundEntities.Register(entityData)
assert(entityData, "Entity data is nil")
local position = entityData.position
assert(position, "Position is nil")
local id = entityData.id or Ids.CreateUniqueId(Entities)
entityData.isServer = isServer
entityData.id = tostring(id)
local entityData = setmetatable(entityData, {
__tostring = function()
return entityData.id
end
})
Entities[id] = entityData
FireFunctions(entityData)
return entityData
end
-- Internal
local function Unregister(id)
Entities[id] = nil
end
function ReboundEntities.GetSyncData(entityData, key)
return entityData[key]
end
--- Get all entityDatas registered
--- @return table = {entityData: table}
function ReboundEntities.GetAll()
return Entities
end
--- Get an entityData by its id
--- @param id string
--- @return table = {model: string, position: vector3, id: string, entity: number, isServer: boolean}
function ReboundEntities.GetById(id)
id = tostring(id)
return Entities[id]
end
--- Get all entities by their model
--- @param model string
--- @return table = {entityData: table}
function ReboundEntities.GetByModel(model)
local entities = {}
for k, v in pairs(Entities) do
if v.model == model then
table.insert(entities, v)
end
end
return entities
end
--- Get the closest entityData to a position
--- @param pos vector3
--- @return table = {model: string, position: vector3, id: string, entity: number, isServer: boolean}
function ReboundEntities.GetClosest(pos)
local closest = nil
local closestDist = 9999
for k, v in pairs(Entities) do
local dist = #(pos - v.pos)
if dist < closestDist then
closest = v
closestDist = dist
end
end
return closest
end
--- Get all entities within a radius of a position
--- @param pos vector3
--- @param radius number
--- @return table = {entityData: table}
function ReboundEntities.GetWithinRadius(pos, radius)
local entities = {}
for k, v in pairs(Entities) do
local dist = #(pos - v.pos)
if dist < radius then
table.insert(entities, v)
end
end
return entities
end
function ReboundEntities.SetOnSyncKeyChange(entityData, cb)
print("SetOnSyncKeyChange", entityData, cb)
local id = entityData.id or entityData
local data = ReboundEntities.GetById(id)
assert(data, "Entity not found")
data.onSyncKeyChange = cb
end
exports('ReboundEntities', ReboundEntities)
if not isServer then goto client end
-- -- -- -- -- -- -- -- -- --
-- ▄▀▀ ██▀ █▀▄ █ █ ██▀ █▀▄ --
-- ▄█▀ █▄▄ █▀▄ ▀▄▀ █▄▄ █▀▄ --
-- -- -- -- -- -- -- -- -- --
--- Create a rebound entity
--- @param entityData object = {model: string, position: vector3, rotation: vector3 || nil}
--- @param src number = Who to send to. Defaults: -1
--- @return table = {model: string, position: vector3, rotation: vector3, id: string, entity: number, isServer: boolean}
function ReboundEntities.Create(entityData, src)
local entity = ReboundEntities.Register(entityData)
assert(entity, "Failed to register entity")
assert(entity.position, "Entity position is nil")
entity.rotation = entity.rotation or vector3(0, 0, 0)
src = src and tonumber(src) or -1
TriggerClientEvent(GetCurrentResourceName() ..":client:CreateReboundEntity", src, entity)
return entity
end
function ReboundEntities.SetCheckRestricted(entityData, cb)
assert(type(cb) == "function", "Check restricted is not a function")
entityData.restricted = cb
end
function ReboundEntities.CheckRestricted(src, entityData)
src = tonumber(src)
if not entityData.restricted then return false end
if src == -1 then return true end
return entityData.restricted(src, entityData) or false
end
function ReboundEntities.Refresh(src)
src= tonumber(src)
local list = ReboundEntities.GetAccessibleList(src)
TriggerClientEvent(GetCurrentResourceName() ..":client:CreateReboundEntities", src, list)
end
--- Delete a rebound entity
--- @param id string = The entity id: string || entityData: object
--- @return boolean
function ReboundEntities.Delete(id)
Unregister(id)
TriggerClientEvent(GetCurrentResourceName() ..":client:DeleteReboundEntity", -1, id)
end
-- Create multiple rebound entities
--- @param entityDatas table = {entityData: object}
--- @param src number = Who to send to. Defaults: -1
--- @return table = {entityData: object}
function ReboundEntities.CreateMultiple(entityDatas, src, restricted)
src = src and tonumber(src) or -1
local _entityDatas = {}
for k, v in pairs(entityDatas) do
local entity = ReboundEntities.Register(v)
assert(entity, "Failed to register entity")
assert(entity.position, "Entity position is nil")
entity.rotation = entity.rotation or vector3(0, 0, 0)
if restricted then
ReboundEntities.SetCheckRestricted(entity, restricted)
end
if not ReboundEntities.CheckRestricted(src, entity) then
_entityDatas[entity.id] = entity
end
end
TriggerClientEvent(GetCurrentResourceName() ..":client:CreateReboundEntities", src, _entityDatas)
return _entityDatas
end
--- Sets data to be synced to clients for a rebound entity
--- @param entityData object = {model: string, position: vector3, rotation: vector3, id: string, entity: number, isServer: boolean}
--- @param key string
--- @param value any
function ReboundEntities.SetSyncData(entityData, key, value)
entityData[key] = value
if entityData.onSyncKeyChange then
entityData.onSyncKeyChange(entityData, key, value)
end
TriggerClientEvent(GetCurrentResourceName() ..":client:SetReboundSyncData", -1, entityData.id, key, value)
end
function ReboundEntities.GetAccessibleList(src)
src = tonumber(src)
local list = {}
for k, data in pairs(ReboundEntities.GetAll()) do
if not ReboundEntities.CheckRestricted(src, data) then
list[data.id] = data
end
end
return list
end
RegisterNetEvent("playerJoining", function(src, _)
ReboundEntities.Refresh(src)
end)
AddEventHandler('onResourceStart', function(resource)
if resource == GetCurrentResourceName() then
Wait(1000)
local players = GetPlayers()
for k, src in pairs(players) do
ReboundEntities.Refresh(src)
end
end
end)
::client::
if isServer then return ReboundEntities end
-- -- -- -- -- -- -- -- -- --
-- ▄▀▀ █ █ ██▀ █▄ █ ▀█▀ --
-- ▀▄▄ █▄▄ █ █▄▄ █ ▀█ █ --
-- -- -- -- -- -- -- -- -- --
--- Loads a model into memory
--- @param model : Model name <string> || Model hash <number>
--- @return modelHash : number
function ReboundEntities.LoadModel(model)
assert(model, "Model is nil")
model = type(model) == "number" and model or GetHashKey(model)
local timeout = 100
RequestModel(model)
while not HasModelLoaded(model) and timeout > 0 do
timeout = timeout - 1
Wait(100)
end
assert(timeout > 0, string.format("Failed to load model %s", model))
return model
end
--- Spawns an entity
--- @param entityData : table = {model: string, position: vector3, rotation: vector3 || nil, onSpawn: function || nil}
--- @return entity : number
function ReboundEntities.Spawn(entityData)
local model = entityData.model
local position = entityData.position
assert(position, "Position is nil")
local rotation = entityData.rotation or vector3(0, 0, 0)
local entity = nil
if model then
model = ReboundEntities.LoadModel(model)
entity = CreateObject(model, position.x, position.y, position.z, false, false, false)
SetEntityRotation(entity, rotation.x, rotation.y, rotation.z)
end
local onSpawn = entityData.onSpawn
print(onSpawn)
if onSpawn then
onSpawn(entityData, entity)
end
-- dont forget to save the entity to the entityData table in the spawn loop
return entity, true
end
--- Set the onSpawn callback for an entity. Triggered after the entity is created.
--- @param id : string || entityData : object
--- @param cb : function(entityData: object, entity: number)
--- @return success : boolean
function ReboundEntities.SetOnSpawn(id, cb)
id = tostring(id)
local entity = ReboundEntities.GetById(id)
assert(entity, "Entity not found")
entity.onSpawn = cb
return true
end
--- Despawns an entity
--- @param entityData : entity || table = {entity: number, onDespawn: function || nil}
function ReboundEntities.Despawn(entityData)
assert(entityData, "Entity data is nil")
local entity = entityData.entity
local onDespawn = entityData.onDespawn
if onDespawn and type(onDespawn) == "function" then
onDespawn(entityData, entity)
end
if entity and DoesEntityExist(entity) then
DeleteEntity(entity)
end
return nil, nil
end
--- Set the onDespawn callback for an entity. Triggered before the entity is deleted.
--- @param id : string || entityData : object
--- @param cb : function(entityData: object, entity: number)
--- @return success : boolean
function ReboundEntities.SetOnDespawn(id, cb)
id = tostring(id)
local entity = ReboundEntities.GetById(id)
assert(entity, "Entity not found")
entity.onDespawn = cb
return true
end
local spawnLoopRunning = false
--- Internal function to spawn entities in a loop
--- @param distanceToCheck : number || nil
--- @param waitTime : number || nil
function ReboundEntities.SpawnLoop(distanceToCheck, waitTime)
if spawnLoopRunning then return end
spawnLoopRunning = true
distanceToCheck = distanceToCheck or 50
waitTime = waitTime or 2500
CreateThread(function()
while spawnLoopRunning do
for k, v in pairs(Entities) do
if not v.restricted then
local pos = GetEntityCoords(PlayerPedId())
local dist = #(pos - v.position)
if dist <= distanceToCheck and (not v.entity and not v.inRange) then
v.entity, v.inRange = ReboundEntities.Spawn(v)
elseif dist > distanceToCheck and (v.entity or v.inRange) then
v.entity, v.inRange = ReboundEntities.Despawn(v)
end
elseif v.entity then
v.entity = ReboundEntities.Despawn(v)
end
end
Wait(waitTime)
end
end)
end
--- Get an entity by its entity number
--- @param entity : number
--- @return entityData : table
function ReboundEntities.GetByEntity(entity)
for k, v in pairs(Entities) do
if v.entity == entity then
return v
end
end
end
--- Create a client side only, rebound entity
--- @param entityData : table = {model: string, position: vector3, rotation: vector3 || nil}
--- @return entity : table
function ReboundEntities.CreateClient(entityData)
local entity = ReboundEntities.Register(entityData)
if not entity then return end
ReboundEntities.SpawnLoop()
return entity
end
--- Delete a client side only, rebound entity
--- @param id : string || entityData : table
--- @return success : boolean
function ReboundEntities.DeleteClient(id)
id = tostring(id)
local entityData = ReboundEntities.GetById(id)
local isServer = entityData.isServer
assert(not isServer, "Cannot delete server entity from client")
local entity = entityData.entity
if entity then
ReboundEntities.Despawn(entityData)
end
Unregister(id)
return true
end
--- Create multiple client only, rebound entities
--- @param entityDatas : table = {entityData: table = {model: string, position: vector3, rotation: vector3 || nil}}
--- @return _entityDatas : objects
function ReboundEntities.CreateMultipleClient(entityDatas)
local _entityDatas = {}
for k, v in pairs(entityDatas) do
local entityData = ReboundEntities.CreateClient(v)
entityDatas[entityData.id] = entityData
end
return _entityDatas
end
--- Delete multiple client only, rebound entities
--- @param ids : table = {string || entityData: table}
function ReboundEntities.DeleteMultipleClient(ids)
for k, v in pairs(ids) do
ReboundEntities.DeleteClient(v)
end
end
-- -- -- -- internal -- -- -- --
local function DeleteFromServer(id)
id = tostring(id)
local entityData = ReboundEntities.GetById(id)
if not entityData then return end
entityData.isServer = nil
ReboundEntities.DeleteClient(id)
end
local function DeleteMultipleFromServer(ids)
for k, v in pairs(ids) do
DeleteFromServer(v)
end
end
-- -- -- -- -- -- -- -- -- --
RegisterNetEvent(GetCurrentResourceName() ..":client:CreateReboundEntity", function(entityData)
ReboundEntities.CreateClient(entityData)
end)
RegisterNetEvent(GetCurrentResourceName() ..":client:DeleteReboundEntity", function(id)
DeleteFromServer(id)
end)
RegisterNetEvent(GetCurrentResourceName() ..":client:CreateReboundEntities", function(entityDatas)
ReboundEntities.CreateMultipleClient(entityDatas)
end)
RegisterNetEvent(GetCurrentResourceName() ..":client:DeleteReboundEntities", function(ids)
DeleteMultipleFromServer(ids)
end)
RegisterNetEvent(GetCurrentResourceName() ..":client:SetReboundSyncData", function(id, key, value)
local entityData = ReboundEntities.GetById(id)
if not entityData then return end
entityData[key] = value
if entityData.onSyncKeyChange then
entityData.onSyncKeyChange(entityData, key, value)
end
end)
AddEventHandler('onResourceStop', function(resource)
if resource == GetCurrentResourceName() then
spawnLoopRunning = false
for k, v in pairs(Entities) do
if v.entity then
DeleteEntity(tonumber(v.entity))
end
end
end
end)
return ReboundEntities