1
0
Fork 0
forked from Simnation/Main
This commit is contained in:
Nordi98 2025-08-12 16:56:50 +02:00
parent 9178871ecd
commit 640cdd069b
9 changed files with 583 additions and 269 deletions

View file

@ -2,8 +2,14 @@ local Entity = Entity
local DebugRendering = false
local DebugInfos = false
-- Used to prevent that the main loop tries to render an entity that has/his been/being deleted
-- (the for each entity itearate over the old entities until next cycle and so will try to render a deleted entity)
local DeletedEntities = {}
local EntitiesLoaded = false
local Entities = {}
--#region Local functions
local GetActiveSlices = function()
local slices = GetSurroundingSlices(currentSlice)
@ -76,41 +82,58 @@ local UnrenderLocalEntity = function(uNetId)
local entity = UtilityNet.GetEntityFromUNetId(uNetId)
if DoesEntityExist(entity) then
TriggerEvent("Utility:Net:OnUnrender", uNetId, entity, GetEntityModel(entity))
local state = Entity(entity).state
Citizen.SetTimeout(1, function()
if not DoesEntityExist(entity) then
if DebugInfos then
warn("UnrenderLocalEntity: entity with uNetId: "..uNetId.." already unrendered, skipping this call")
if not state.preserved then
TriggerEvent("Utility:Net:OnUnrender", uNetId, entity, GetEntityModel(entity))
end
if not DoesEntityExist(entity) then
if DebugInfos then
warn("UnrenderLocalEntity: entity with uNetId: "..uNetId.." already unrendered, skipping this call")
end
return
end
-- Remove state change handler (currently used only for attaching)
if state.changeHandler then
UtilityNet.RemoveStateBagChangeHandler(state.changeHandler)
state.changeHandler = nil
end
if state.found then
if state.door then
if DoesEntityExist(state.door) then
SetEntityVisible(state.door, true)
SetEntityCollision(state.door, true, true)
end
return
end
local state = Entity(entity).state
-- Remove state change handler (currently used only for attaching)
if state.changeHandler then
UtilityNet.RemoveStateBagChangeHandler(state.changeHandler)
state.changeHandler = nil
end
if state.found then
else
local model = GetEntityModel(entity)
-- Show map object
RemoveModelHide(GetEntityCoords(entity), 0.1, model)
end
if state.preserved then
SetEntityAsNoLongerNeeded(entity)
else
DeleteEntity(entity)
end
state.rendered = false
EntitiesStates[uNetId] = nil
TriggerLatentServerEvent("Utility:Net:RemoveStateListener", 5120, uNetId)
end)
end
if not state.preserved then
DeleteEntity(entity)
end
state.rendered = false
EntitiesStates[uNetId] = nil
TriggerLatentServerEvent("Utility:Net:RemoveStateListener", 5120, uNetId)
if state.preserved then
TriggerEvent("Utility:Net:OnUnrender", uNetId, entity, GetEntityModel(entity))
-- Max 5000ms for the entity to be deleted if it was preserved, if it still exists, delete it (this prevents entity leaks)
Citizen.SetTimeout(5000, function()
if DoesEntityExist(entity) then
warn("UnrenderLocalEntity: entity with uNetId: "..uNetId.." was preserved for more than 5 seconds, deleting it now")
DeleteEntity(entity)
end
end)
end
end
LocalEntities[uNetId] = nil
@ -141,17 +164,44 @@ local RenderLocalEntity = function(uNetId, entityData)
local model = entityData.model
local options = entityData.options
if not options.replace then
if not IsModelValid(model) then
error("RenderLocalEntity: Model "..model.." is not valid, uNetId: "..uNetId)
if options.abstract then
if options.replace then
error("RenderLocalEntity: abstract entities can't have the \"replace\" option, uNetId: "..uNetId.." model: "..model)
end
if not IsModelValid(model) then
RegisterArchetypes(function()
return {
{
flags = 139296,
bbMin = vector3(-0.1, -0.1, -0.1),
bbMax = vector3(0.1, 0.1, 0.1),
bsCentre = vector3(0.0, 0.0, 0.0),
bsRadius = 1.0,
name = model,
textureDictionary = '',
physicsDictionary = '',
assetName = model,
assetType = 'ASSET_TYPE_DRAWABLE',
lodDist = 999,
specialAttribute = 0
}
}
end)
end
end
if not IsModelValid(model) then
error("RenderLocalEntity: Model "..tostring(model).." is not valid, uNetId: "..uNetId)
end
if not options.abstract then
local start = GetGameTimer()
while not HasModelLoaded(model) do
if (GetGameTimer() - start) > 5000 then
error("RenderLocalEntity: Model "..model.." failed to load, uNetId: "..uNetId)
end
RequestModel(model)
Citizen.Wait(1)
end
@ -188,15 +238,23 @@ local RenderLocalEntity = function(uNetId, entityData)
local distance = options.door and 1.5 or 0.1
if options.door and interior ~= 0 then
Entity(obj).state.door = _obj
-- Doors inside interiors need to be deleted
-- If not deleted the game will be recreate them every time the interior is reloaded (player exit and then re-enter)
-- And so there will be 2 copies of the same door
DeleteEntity(_obj)
SetEntityVisible(_obj, false)
SetEntityCollision(_obj, false, false)
else
CreateModelHideExcludingScriptObjects(coords, distance, model)
end
else
obj = CreateObject(model, coords, false, false, options.door)
if options.abstract then
obj = old_CreateObject(model, coords, false, false, options.door)
else
obj = CreateObject(model, coords, false, false, options.door)
end
SetEntityCoords(obj, coords) -- This is required to ignore the pivot
end
@ -210,6 +268,10 @@ local RenderLocalEntity = function(uNetId, entityData)
SetEntityRotation(obj, options.rotation)
end
if options.abstract then
Entity(obj).state.abstract_model = model
end
-- Always listen for __attached changes (attach/detach)
state.changeHandler = UtilityNet.AddStateBagChangeHandler(uNetId, function(key, value)
-- Exit if entity is no longer valid
@ -226,10 +288,12 @@ local RenderLocalEntity = function(uNetId, entityData)
--print("Detach")
DetachEntity(obj, true, true)
end
LocalEntities[uNetId].attached = value
end
end)
LocalEntities[uNetId] = {obj=obj, slice=entityData.slice}
LocalEntities[uNetId] = {obj=obj, slice=entityData.slice, createdBy = entityData.createdBy, attached = state.__attached}
-- Fetch initial state
ServerRequestEntityStates(uNetId)
@ -260,28 +324,26 @@ local CanEntityBeRendered = function(uNetId, entityData, slices)
end
-- Check if entity is within drawing slices (if provided)
if slices and not slices[entityData.slice] then
if slices and not table.find(slices, entityData.slice) then
return false
end
local state = UtilityNet.State(uNetId)
if DeletedEntities[uNetId] then
return false
end
-- Render only if within render distance
if not state.__attached then
local coords = GetEntityCoords(PlayerPedId())
local modelsRenderDistance = GlobalState.ModelsRenderDistance
local renderDistance = modelsRenderDistance[entityData.model] or 50.0
if #(entityData.coords - coords) > renderDistance then
return false
end
-- Skip distance check if entity is rendered and attached (keep them alive)
if LocalEntities[uNetId] and LocalEntities[uNetId].attached then
return true
end
return true
local coords = GetEntityCoords(PlayerPedId())
local modelsRenderDistance = GlobalState.ModelsRenderDistance
local hashmodel = type(entityData.model) == "number" and entityData.model or GetHashKey(entityData.model)
local renderDistance = modelsRenderDistance[hashmodel] or 50.0
return #(entityData.coords - coords) < renderDistance
end
--#endregion
@ -315,7 +377,6 @@ StartUtilityNetRenderLoop = function()
-- Render/Unrender near slices entities
UtilityNet.ForEachEntity(function(v)
nEntities = nEntities + 1
if not LocalEntities[v.id] and CanEntityBeRendered(v.id, v) then
local obj = UtilityNet.GetEntityFromUNetId(v.id) or 0
local state = Entity(obj).state or {}
@ -342,10 +403,8 @@ StartUtilityNetRenderLoop = function()
-- Unrender entities that are out of slice
-- Run only if the slice has changed (so something can be out of the slice and need to be unrendered)
if lastSlice ~= currentSlice then
local entities = GlobalState.Entities
for netId, data in pairs(LocalEntities) do
local entityData = entities[data.slice][netId]
local entityData = Entities[data.slice][netId]
if not CanEntityBeRendered(netId, entityData) then
UnrenderLocalEntity(netId)
@ -368,8 +427,35 @@ StartUtilityNetRenderLoop = function()
end
RegisterNetEvent("Utility:Net:RefreshModel", function(uNetId, model)
local timeout = 3000
local start = GetGameTimer()
while not LocalEntities[uNetId] and (GetGameTimer() - start < 3000) do
local entity, slice = nil
while not entity or not slice do
entity, slice = UtilityNet.InternalFindFromNetId(uNetId)
if (GetGameTimer() - start) > timeout then
error("UtilityNet:RefreshModel: Entity existance check timed out for uNetId "..tostring(uNetId))
break
end
Citizen.Wait(1)
end
if entity and Entities[slice] then
Entities[slice][uNetId].model = model
else
error(
"Utility:Net:RefreshModel: Entity not found for uNetId " .. tostring(uNetId) ..
" setting model " .. tostring(model) ..
" entity: " .. tostring(entity) ..
", slice: " .. tostring(slice) ..
", doesExist? " .. tostring(UtilityNet.DoesUNetIdExist(uNetId))
)
end
start = GetGameTimer()
while not LocalEntities[uNetId] and (GetGameTimer() - start < timeout) do
Citizen.Wait(1)
end
@ -390,13 +476,6 @@ RegisterNetEvent("Utility:Net:RefreshModel", function(uNetId, model)
-- Tamper with the entity model and render again
local entityData = UtilityNet.InternalFindFromNetId(uNetId)
if not entityData then
error("RefreshModel: entity with uNetId: "..tostring(uNetId).." cant be found")
return
end
entityData.model = model
SetNetIdBeingBusy(uNetId, false)
RenderLocalEntity(uNetId, entityData)
@ -415,68 +494,108 @@ RegisterNetEvent("Utility:Net:RefreshModel", function(uNetId, model)
end
end)
RegisterNetEvent("Utility:Net:RefreshCoords", function(uNetId, coords)
RegisterNetEvent("Utility:Net:RefreshCoords", function(uNetId, coords, skipPositionUpdate)
local start = GetGameTimer()
while not LocalEntities[uNetId] and (GetGameTimer() - start < 3000) do
Citizen.Wait(1)
end
if LocalEntities[uNetId] then
while not UtilityNet.IsReady(uNetId) or IsNetIdBusy(uNetId) do
Citizen.Wait(100)
end
local entity, slice = UtilityNet.InternalFindFromNetId(uNetId)
SetNetIdBeingBusy(uNetId, true)
SetEntityCoords(LocalEntities[uNetId].obj, coords)
SetNetIdBeingBusy(uNetId, false)
end
end)
if entity and Entities[slice] then
local newSlice = GetSliceFromCoords(coords)
RegisterNetEvent("Utility:Net:RefreshRotation", function(uNetId, rotation)
local start = GetGameTimer()
while not LocalEntities[uNetId] and (GetGameTimer() - start < 3000) do
Citizen.Wait(1)
end
if LocalEntities[uNetId] then
while not UtilityNet.IsReady(uNetId) or IsNetIdBusy(uNetId) do
Citizen.Wait(100)
end
SetNetIdBeingBusy(uNetId, true)
SetEntityRotation(LocalEntities[uNetId].obj, rotation)
SetNetIdBeingBusy(uNetId, false)
end
end)
RegisterNetEvent("Utility:Net:EntityCreated", function(_callId, uNetId)
local attempts = 0
while not UtilityNet.DoesUNetIdExist(uNetId) do
attempts = attempts + 1
if attempts > 5 then
if DebugRendering then
error("EntityCreated", uNetId, "id not found after 10 attempts")
if newSlice ~= slice then
local entity = Entities[slice][uNetId]
Entities[slice][uNetId] = nil
if not Entities[newSlice] then
Entities[newSlice] = {}
end
return
Entities[newSlice][uNetId] = entity
slice = newSlice
end
Citizen.Wait(100)
Entities[slice][uNetId].coords = coords
Entities[slice][uNetId].slice = newSlice
end
if CanEntityBeRendered(uNetId) then
if not skipPositionUpdate then
while not LocalEntities[uNetId] and (GetGameTimer() - start < 3000) do
Citizen.Wait(1)
end
if LocalEntities[uNetId] then
while not UtilityNet.IsReady(uNetId) or IsNetIdBusy(uNetId) do
Citizen.Wait(100)
end
SetNetIdBeingBusy(uNetId, true)
SetEntityCoords(LocalEntities[uNetId].obj, coords)
SetNetIdBeingBusy(uNetId, false)
end
end
end)
RegisterNetEvent("Utility:Net:RefreshRotation", function(uNetId, rotation, skipRotationUpdate)
local start = GetGameTimer()
local entity, slice = UtilityNet.InternalFindFromNetId(uNetId)
if entity and Entities[slice] then
Entities[slice][uNetId].options.rotation = rotation
end
if not skipRotationUpdate then
while not LocalEntities[uNetId] and (GetGameTimer() - start < 3000) do
Citizen.Wait(1)
end
if LocalEntities[uNetId] then
while not UtilityNet.IsReady(uNetId) or IsNetIdBusy(uNetId) do
Citizen.Wait(100)
end
SetNetIdBeingBusy(uNetId, true)
SetEntityRotation(LocalEntities[uNetId].obj, rotation)
SetNetIdBeingBusy(uNetId, false)
end
end
end)
RegisterNetEvent("Utility:Net:EntityCreated", function(_callId, object)
local uNetId = object.id
local slices = GetActiveSlices()
if not Entities[object.slice] then
Entities[object.slice] = {}
end
Entities[object.slice][object.id] = object
if CanEntityBeRendered(uNetId, object, slices) then
if DebugRendering then
print("RenderLocalEntity", uNetId, "EntityCreated")
end
RenderLocalEntity(uNetId)
RenderLocalEntity(uNetId, object)
end
end)
RegisterNetEvent("Utility:Net:RequestDeletion", function(uNetId)
local entityData = UtilityNet.InternalFindFromNetId(uNetId)
if not entityData then return end
local slice = GetSliceFromCoords(entityData.coords)
if LocalEntities[uNetId] then
DeletedEntities[uNetId] = true
UnrenderLocalEntity(uNetId)
if Entities[slice] then
Entities[slice][uNetId] = nil
end
else
if Entities[slice] then
Entities[slice][uNetId] = nil
end
end
end)
@ -487,6 +606,39 @@ Citizen.CreateThread(function()
end
end)
Citizen.CreateThread(function()
RegisterNetEvent("Utility:Net:GetEntities", function(entities)
Entities = entities
EntitiesLoaded = true
end)
TriggerServerEvent("Utility:Net:GetEntities")
end)
AddEventHandler("onResourceStop", function(resource)
if resource == GetCurrentResourceName() then
for k,v in pairs(LocalEntities) do
Citizen.CreateThreadNow(function()
DeletedEntities[k] = true
UnrenderLocalEntity(k)
end)
end
else
for k,v in pairs(LocalEntities) do
if v.createdBy == resource then
if DebugRendering then
print("Unrendering deleted entity", k)
end
Citizen.CreateThreadNow(function()
DeletedEntities[k] = true
UnrenderLocalEntity(k)
end)
end
end
end
end)
-- Exports
UtilityNet.GetEntityFromUNetId = function(uNetId)
return LocalEntities[uNetId]?.obj
@ -500,6 +652,37 @@ UtilityNet.GetUNetIdFromEntity = function(entity)
end
end
UtilityNet.GetuNetIdCreator = function(uNetId)
return LocalEntities[uNetId]?.createdBy
end
UtilityNet.GetEntityCreator = function(entity)
return UtilityNet.GetuNetIdCreator(UtilityNet.GetUNetIdFromEntity(entity))
end
UtilityNet.InternalFindFromNetId = function(uNetId)
for sliceI, slice in pairs(Entities) do
if slice[uNetId] then
return slice[uNetId], sliceI
end
end
end
exports("GetEntityFromUNetId", UtilityNet.GetEntityFromUNetId)
exports("GetUNetIdFromEntity", UtilityNet.GetUNetIdFromEntity)
exports("GetuNetIdCreator", UtilityNet.GetuNetIdCreator)
exports("GetEntityCreator", UtilityNet.GetEntityCreator)
exports("GetRenderedEntities", function() return LocalEntities end)
exports("GetEntities", function(slice)
while not EntitiesLoaded do
Citizen.Wait(1)
end
if slice then
return Entities[slice] or {}
else
return Entities
end
end)
exports("InternalFindFromNetId", UtilityNet.InternalFindFromNetId)

View file

@ -1,6 +1,25 @@
EntitiesStates = {}
local function IsEntityStateLoaded(uNetId)
return EntitiesStates[uNetId] ~= -1
end
local function EnsureStateLoaded(uNetId)
if not IsEntityStateLoaded(uNetId) then
local start = GetGameTimer()
while not IsEntityStateLoaded(uNetId) do
if GetGameTimer() - start > 5000 then
error("WaitUntilStateLoaded: entity "..tostring(uNetId).." state loading timed out")
break
end
Citizen.Wait(1)
end
end
end
RegisterNetEvent("Utility:Net:UpdateStateValue", function(uNetId, key, value)
EnsureStateLoaded(uNetId)
if not EntitiesStates[uNetId] then
EntitiesStates[uNetId] = {}
end
@ -9,14 +28,49 @@ RegisterNetEvent("Utility:Net:UpdateStateValue", function(uNetId, key, value)
end)
GetEntityStateValue = function(uNetId, key)
if not EntitiesStates[uNetId] then
return
if not UtilityNet.GetEntityFromUNetId(uNetId) then -- If trying to get state of entity that isnt loaded
local start = GetGameTimer()
local entity = nil
while not entity do
entity = UtilityNet.InternalFindFromNetId(uNetId)
if GetGameTimer() - start > 2000 then
error("GetEntityStateValue: entity "..tostring(uNetId).." doesnt exist, attempted to retrieve key: "..tostring(key))
break
end
Citizen.Wait(1)
end
return ServerRequestEntityKey(uNetId, key)
else
EnsureStateLoaded(uNetId)
if not EntitiesStates[uNetId] then
warn("GetEntityStateValue: entity "..tostring(uNetId).." has no loaded states, attempted to retrieve key: "..tostring(key))
return
end
return EntitiesStates[uNetId][key]
end
return EntitiesStates[uNetId][key]
end
ServerRequestEntityKey = function(uNetId, key)
local p = promise:new()
local event = nil
event = RegisterNetEvent("Utility:Net:GetStateValue"..uNetId, function(value)
RemoveEventHandler(event)
p:resolve(value)
end)
TriggerServerEvent("Utility:Net:GetStateValue", uNetId, key)
return Citizen.Await(p)
end
ServerRequestEntityStates = function(uNetId)
EntitiesStates[uNetId] = -1 -- Set as loading
local p = promise:new()
local event = nil
@ -28,7 +82,7 @@ ServerRequestEntityStates = function(uNetId)
TriggerServerEvent("Utility:Net:GetState", uNetId)
local states = Citizen.Await(p)
EntitiesStates[uNetId] = states
EntitiesStates[uNetId] = states or {}
end
exports("GetEntityStateValue", GetEntityStateValue)

View file

@ -1447,6 +1447,28 @@ end
return values
end
-- Uses table.clone for fast shallow copying (memcpy) before checking and doing actual deepcopy for nested tables
-- Handles circular references via seen table
-- Significantly faster (~50%) than doing actual deepcopy for flat or lightly-nested structures
---@param orig table
---@return table
table.deepcopy = function(orig, seen)
if type(orig) ~= "table" then return orig end
seen = seen or {}
if seen[orig] then return seen[orig] end
local copy = table.clone(orig)
seen[orig] = copy
for k, v in next, orig do
if type(v) == "table" then
copy[k] = table.deepcopy(v, seen)
end
end
return copy
end
math.round = function(number, decimals)
local _ = 10 ^ decimals
@ -2512,7 +2534,8 @@ end
NetworkRequestControlOfEntity(trolly)
end
local bagObj = CreateObject("hei_p_m_bag_var22_arm_s", vector3(0.0, 0.0, 0.0), true)
local playerCoords = GetEntityCoords(ped)
local bagObj = CreateObject("hei_p_m_bag_var22_arm_s", playerCoords + vector3(0.0, 0.0, -6.0), true)
SetEntityCollision(bagObj, false, true)
-- Intro
@ -2984,13 +3007,26 @@ end
--// UtilityNet //
local CreatedEntities = {}
local old_GetEntityArchetypeName = GetEntityArchetypeName
GetEntityArchetypeName = function(entity)
if not entity or not DoesEntityExist(entity) then
return ""
end
local res = old_GetEntityArchetypeName(entity)
if res == "" then
return Entity(entity)?.state?.abstract_model or res
else
return res
end
end
--#region API
UtilityNet.ForEachEntity = function(fn, slices)
if slices then
local entities = GlobalState.Entities
for i = 1, #slices do
local _entities = entities[slices[i]]
local _entities = UtilityNet.GetEntities(slices[i])
local n = 0
if _entities then
@ -3009,7 +3045,7 @@ UtilityNet.ForEachEntity = function(fn, slices)
end
end
else
local entities = GlobalState.Entities
local entities = UtilityNet.GetEntities()
if not entities then
return
@ -3035,14 +3071,6 @@ UtilityNet.ForEachEntity = function(fn, slices)
end
end
UtilityNet.InternalFindFromNetId = function(uNetId)
for sliceI, slice in pairs(GlobalState.Entities) do
if slice[uNetId] then
return slice[uNetId], sliceI
end
end
end
UtilityNet.SetDebug = function(state)
UtilityNetDebug = state
@ -3071,7 +3099,9 @@ UtilityNet.SetDebug = function(state)
for k,v in pairs(localEntities) do
local state = UtilityNet.State(v.netId)
DrawText3Ds(GetEntityCoords(v.obj), "NetId: "..v.netId, 0.25)
if DoesEntityExist(v.obj) then
DrawText3Ds(GetEntityCoords(v.obj), "NetId: "..v.netId, 0.25)
end
end
Citizen.Wait(1)
end
@ -3089,16 +3119,16 @@ UtilityNet.CreateEntity = function(model, coords, options)
-- Set resource name in options
options = options or {}
options.resource = GetCurrentResourceName()
options.createdBy = GetCurrentResourceName()
local callId = math.random(0, 10000000)
local event = nil
local entity = promise:new()
-- Callback
event = RegisterNetEvent("Utility:Net:EntityCreated", function(_callId, uNetId)
event = RegisterNetEvent("Utility:Net:EntityCreated", function(_callId, object)
if _callId == callId then
entity:resolve(uNetId)
entity:resolve(object.id)
RemoveEventHandler(event)
end
end)
@ -3207,18 +3237,18 @@ UtilityNet.DetachEntity = function(uNetId)
end
-- Using a latent event to prevent blocking the network channel
UtilityNet.SetEntityCoords = function(uNetId, coords)
UtilityNet.SetEntityCoords = function(uNetId, coords, skipPositionUpdate)
TriggerLatentServerEvent("Utility:Net:SetEntityCoords", 5120, uNetId, coords)
-- Instantly sync for local obj
TriggerEvent("Utility:Net:RefreshCoords", uNetId, coords)
TriggerEvent("Utility:Net:RefreshCoords", uNetId, coords, skipPositionUpdate)
end
UtilityNet.SetEntityRotation = function(uNetId, rot)
TriggerLatentServerEvent("Utility:Net:SetEntityRotation", 5120, uNetId, rot)
UtilityNet.SetEntityRotation = function(uNetId, rot, skipRotationUpdate)
TriggerLatentServerEvent("Utility:Net:SetEntityRotation", 5120, uNetId, rot, skipRotationUpdate)
-- Instantly sync for local obj
TriggerEvent("Utility:Net:RefreshRotation", uNetId, rot)
TriggerEvent("Utility:Net:RefreshRotation", uNetId, rot, skipRotationUpdate)
end
UtilityNet.SetEntityModel = function(uNetId, model)
@ -3322,6 +3352,22 @@ end
UtilityNet.GetUNetIdFromEntity = function(entity)
return exports["utility_lib"]:GetUNetIdFromEntity(entity)
end
UtilityNet.GetuNetIdCreator = function(uNetId)
return exports["utility_lib"]:GetuNetIdCreator(uNetId)
end
UtilityNet.GetEntityCreator = function(entity)
return exports["utility_lib"]:GetEntityCreator(entity)
end
UtilityNet.InternalFindFromNetId = function(uNetId)
return exports["utility_lib"]:InternalFindFromNetId(uNetId)
end
UtilityNet.GetEntities = function(slice)
return exports["utility_lib"]:GetEntities(slice)
end
--#endregion
--#endregion

View file

@ -1,4 +1,5 @@
local NextId = 1
local Entities = {}
UtilityNet = UtilityNet or {}
@ -10,24 +11,27 @@ UtilityNet = UtilityNet or {}
-- }
UtilityNet.CreateEntity = function(model, coords, options, callId)
options = options or {}
local hashmodel = nil
--#region Checks
if not model or (type(model) ~= "string" and type(model) ~= "number") then
error("Invalid model, got "..type(model).." expected string or number", 0)
else
if type(model) == "string" then
model = GetHashKey(model)
hashmodel = GetHashKey(model)
else
hashmodel = model
end
end
if not coords or type(coords) ~= "vector3" then
error("Invalid coords, got "..type(coords).." expected vector3", 0)
end
options = options or {}
--#endregion
--#region Event
TriggerEvent("Utility:Net:EntityCreating", model, coords, options)
TriggerEvent("Utility:Net:EntityCreating", hashmodel, coords, options)
-- EntityCreating event can be canceled, in that case we dont create the entity
if WasEventCanceled() then
@ -35,29 +39,27 @@ UtilityNet.CreateEntity = function(model, coords, options, callId)
end
--#endregion
local entities = GlobalState.Entities
local slice = GetSliceFromCoords(coords)
local object = {
id = NextId,
model = model,
model = options.abstract and model or hashmodel,
coords = coords,
slice = slice,
options = options,
createdBy = options.resource or GetInvokingResource(),
createdBy = options.createdBy or GetInvokingResource(),
}
if not entities[slice] then
entities[slice] = {}
if not Entities[slice] then
Entities[slice] = {}
end
entities[slice][object.id] = object
GlobalState.Entities = entities
Entities[slice][object.id] = object
RegisterEntityState(object.id)
NextId = NextId + 1
TriggerLatentClientEvent("Utility:Net:EntityCreated", -1, 5120, callId, object.id)
TriggerLatentClientEvent("Utility:Net:EntityCreated", -1, 5120, callId, object)
return object.id
end
@ -85,103 +87,32 @@ UtilityNet.DeleteEntity = function(uNetId)
--#endregion
local entities = GlobalState.Entities
local entity = UtilityNet.InternalFindFromNetId(uNetId)
if entity then
entities[entity.slice][entity.id] = nil
Entities[entity.slice][entity.id] = nil
end
GlobalState.Entities = entities
TriggerLatentEventForListeners("Utility:Net:RequestDeletion", uNetId, 5120, uNetId)
TriggerLatentClientEvent("Utility:Net:RequestDeletion", -1, 5120, uNetId)
ClearEntityStates(uNetId) -- Clear states after trigger
end
local queues = {
ModelsRenderDistance = {},
Entities = {},
}
local function StartQueueUpdateLoop(bagkey)
local queue = queues[bagkey]
Citizen.CreateThread(function()
while queue.updateLoop do
-- Nothing added in the last 100ms
if (GetGameTimer() - queue.lastInt) > 200 then
local old = GlobalState[bagkey]
if bagkey == "Entities" then
UtilityNet.ForEachEntity(function(entity)
if queue[entity.id] then
-- Rotation need to be handled separately
if queue[entity.id].rotation then
old[entity.slice][entity.id].options.rotation = queue[entity.id].rotation
queue[entity.id].rotation = nil
end
for k,v in pairs(queue[entity.id]) do
-- If slice need to be updated, move entity to new slice
if k == "slice" and v ~= entity.slice then
local newSlice = v
old[newSlice][entity.id] = old[entity.slice][entity.id] -- Copy to new slice
old[entity.slice][entity.id] = nil -- Remove from old
entity = old[newSlice][entity.id] -- Update entity variable
end
old[entity.slice][entity.id][k] = v
end
end
end)
else
for k,v in pairs(old) do
-- Net id need to be updated
if queue[v.id] then
-- Rotation need to be handled separately
if queue[v.id].rotation then
v.options.rotation = queue[v.id].rotation
queue[v.id].rotation = nil
end
for k2,v2 in pairs(queue[v.id]) do
v[k2] = v2
end
end
end
end
-- Refresh GlobalState
GlobalState[bagkey] = old
queues[bagkey].updateLoop = false
queues[bagkey] = {}
end
Citizen.Wait(150)
UtilityNet.InternalFindFromNetId = function(uNetId)
for sliceI, slice in pairs(Entities) do
if slice[uNetId] then
return slice[uNetId], sliceI
end
end)
end
end
local function InsertValueInQueue(bagkey, id, value)
-- If it is already in the queue with some values that need to be updated, we merge the 2 updates into 1
if queues[bagkey][id] then
queues[bagkey][id] = table.merge(queues[bagkey][id], value)
UtilityNet.GetEntities = function(slice)
if slice then
return Entities[slice]
else
queues[bagkey][id] = value
end
queues[bagkey].lastInt = GetGameTimer()
if not queues[bagkey].updateLoop then
queues[bagkey].updateLoop = true
StartQueueUpdateLoop(bagkey)
return Entities
end
end
UtilityNet.SetModelRenderDistance = function(model, distance)
if type(model) == "string" then
model = GetHashKey(model)
@ -192,30 +123,57 @@ UtilityNet.SetModelRenderDistance = function(model, distance)
GlobalState.ModelsRenderDistance = _
end
UtilityNet.SetEntityRotation = function(uNetId, newRotation)
UtilityNet.SetEntityRotation = function(uNetId, newRotation, skipRotationUpdate)
local source = source
if type(newRotation) ~= "vector3" then
error("Invalid rotation, got "..type(newRotation).." expected vector3", 2)
end
InsertValueInQueue("Entities", uNetId, {rotation = newRotation})
if newRotation.x ~= newRotation.x or newRotation.y ~= newRotation.y or newRotation.z ~= newRotation.z then
error("Invalid rotation, got "..type(newRotation).." (with NaN) expected vector3", 2)
end
local entity, slice = UtilityNet.InternalFindFromNetId(uNetId)
Entities[slice][uNetId].options.rotation = newRotation
-- Except caller since it will be already updated
TriggerLatentEventForListenersExcept("Utility:Net:RefreshRotation", uNetId, 5120, source, uNetId, newRotation)
TriggerLatentClientEvent("Utility:Net:RefreshRotation", -1, 5120, uNetId, newRotation, skipRotationUpdate)
end
UtilityNet.SetEntityCoords = function(uNetId, newCoords)
UtilityNet.SetEntityCoords = function(uNetId, newCoords, skipPositionUpdate)
local source = source
if type(newCoords) ~= "vector3" then
error("Invalid coords, got "..type(newCoords).." expected vector3", 2)
end
InsertValueInQueue("Entities", uNetId, {coords = newCoords, slice = GetSliceFromCoords(newCoords)})
if newCoords.x ~= newCoords.x or newCoords.y ~= newCoords.y or newCoords.z ~= newCoords.z then
error("Invalid coords, got "..type(newCoords).." (with NaN) expected vector3", 2)
end
local entity, slice = UtilityNet.InternalFindFromNetId(uNetId)
local newSlice = GetSliceFromCoords(newCoords)
if newSlice ~= slice then
local old = Entities[slice][uNetId]
if not Entities[newSlice] then
Entities[newSlice] = {}
end
Entities[slice][uNetId] = nil
Entities[newSlice][uNetId] = old
slice = newSlice
end
Entities[slice][uNetId].coords = newCoords
Entities[slice][uNetId].slice = GetSliceFromCoords(newCoords)
-- Except caller since it will be already updated
TriggerLatentEventForListenersExcept("Utility:Net:RefreshCoords", uNetId, 5120, source, uNetId, newCoords)
TriggerLatentClientEvent("Utility:Net:RefreshCoords", -1, 5120, uNetId, newCoords, skipPositionUpdate)
end
UtilityNet.SetEntityModel = function(uNetId, model)
@ -229,10 +187,12 @@ UtilityNet.SetEntityModel = function(uNetId, model)
model = GetHashKey(model)
end
InsertValueInQueue("Entities", uNetId, {model = model})
local entity, slice = UtilityNet.InternalFindFromNetId(uNetId)
Entities[slice][uNetId].model = model
-- Except caller since it will be already updated
TriggerLatentEventForListenersExcept("Utility:Net:RefreshModel", uNetId, 5120, source, uNetId, model)
TriggerLatentClientEvent("Utility:Net:RefreshModel", -1, 5120, uNetId, model)
end
--#region Events
@ -244,7 +204,7 @@ UtilityNet.RegisterEvents = function()
RegisterNetEvent("Utility:Net:DeleteEntity", function(uNetId)
UtilityNet.DeleteEntity(uNetId)
end)
RegisterNetEvent("Utility:Net:SetModelRenderDistance", function(model, distance)
UtilityNet.SetModelRenderDistance(model, distance)
end)
@ -274,13 +234,8 @@ UtilityNet.RegisterEvents = function()
RegisterNetEvent("Utility:Net:SetEntityModel", UtilityNet.SetEntityModel)
RegisterNetEvent("Utility:Net:SetEntityRotation", UtilityNet.SetEntityRotation)
-- Clear all entities on resource stop
AddEventHandler("onResourceStop", function(resource)
if resource == GetCurrentResourceName() then
UtilityNet.ForEachEntity(function(v)
TriggerLatentEventForListeners("Utility:Net:RequestDeletion", v, 5120, v)
end)
end
RegisterNetEvent("Utility:Net:GetEntities", function()
TriggerClientEvent("Utility:Net:GetEntities", source, UtilityNet.GetEntities())
end)
end
--#endregion
@ -289,6 +244,8 @@ end
exports("CreateEntity", UtilityNet.CreateEntity)
exports("DeleteEntity", UtilityNet.DeleteEntity)
exports("SetModelRenderDistance", UtilityNet.SetModelRenderDistance)
exports("GetEntities", UtilityNet.GetEntities)
exports("InternalFindFromNetId", UtilityNet.InternalFindFromNetId)
exports("SetEntityModel", UtilityNet.SetEntityModel)
exports("SetEntityCoords", UtilityNet.SetEntityCoords)

View file

@ -44,6 +44,7 @@ UpdateStateValueForListeners = function(uNetId, key, value)
end
for k,v in pairs(EntitiesStates[uNetId].listeners) do
TriggerEvent("Utility:Net:UpdateStateValue", uNetId, key, value)
TriggerClientEvent("Utility:Net:UpdateStateValue", v, uNetId, key, value)
end
end
@ -161,6 +162,7 @@ RegisterNetEvent("Utility:Net:GetState", function(uNetId)
local source = source
if not EntitiesStates[uNetId] then
warn("GetState: No state found for "..uNetId)
TriggerClientEvent("Utility:Net:GetState"..uNetId, source, nil)
return
end
@ -168,6 +170,19 @@ RegisterNetEvent("Utility:Net:GetState", function(uNetId)
ListenStateUpdates(source, uNetId)
TriggerClientEvent("Utility:Net:GetState"..uNetId, source, EntitiesStates[uNetId].states)
end)
-- Single value
RegisterNetEvent("Utility:Net:GetStateValue", function(uNetId, key)
local source = source
if not EntitiesStates[uNetId] then
warn("GetStateValue: No state found for "..uNetId)
TriggerClientEvent("Utility:Net:GetStateValue"..uNetId, source, nil)
return
end
TriggerClientEvent("Utility:Net:GetStateValue"..uNetId, source, EntitiesStates[uNetId].states[key])
end)
--#endregion
-- On player disconnect remove all listeners of that player (prevent useless bandwidth usage)

View file

@ -1,5 +1,4 @@
GlobalState.ModelsRenderDistance = {}
GlobalState.Entities = {}
LoadUtilityFrameworkIfFound()

View file

@ -30,23 +30,23 @@
--// Player //--
-- Item
AddItem = function(source, ...)
AddItem = function(source, item, amount, metadata, slot)
if ESX then
xPlayer = ESX.GetPlayerFromId(source)
xPlayer.addInventoryItem(...)
xPlayer.addInventoryItem(item, amount, metadata, slot)
else
xPlayer = QBCore.Functions.GetPlayer(source)
xPlayer.Functions.AddItem(...)
xPlayer.Functions.AddItem(item, amount, slot, metadata)
end
end
RemoveItem = function(source, ...)
RemoveItem = function(source, item, amount, metadata, slot)
if ESX then
xPlayer = ESX.GetPlayerFromId(source)
xPlayer.removeInventoryItem(...)
xPlayer.removeInventoryItem(item, amount, metadata, slot)
else
xPlayer = QBCore.Functions.GetPlayer(source)
xPlayer.Functions.RemoveItem(...)
xPlayer.Functions.RemoveItem(item, amount, slot, metadata)
end
end
@ -616,6 +616,28 @@
return values
end
-- Uses table.clone for fast shallow copying (memcpy) before checking and doing actual deepcopy for nested tables
-- Handles circular references via seen table
-- Significantly faster (~50%) than doing actual deepcopy for flat or lightly-nested structures
---@param orig table
---@return table
table.deepcopy = function(orig, seen)
if type(orig) ~= "table" then return orig end
seen = seen or {}
if seen[orig] then return seen[orig] end
local copy = table.clone(orig)
seen[orig] = copy
for k, v in next, orig do
if type(v) == "table" then
copy[k] = table.deepcopy(v, seen)
end
end
return copy
end
math.round = function(number, decimals)
local _ = 10 ^ decimals
return math.floor((number * _) + 0.5) / (_)
@ -744,32 +766,52 @@
local CreatedEntities = {}
UtilityNet = {}
UtilityNet.ForEachEntity = function(fn, slice)
if slice then
if not GlobalState.Entities[slice] then
return
end
UtilityNet.ForEachEntity = function(fn, slices)
if slices then
local entities = UtilityNet.GetEntities(slices)
for k,v in pairs(GlobalState.Entities[slice]) do
local ret = fn(v, k)
for i = 1, #slices do
local _entities = entities[slices[i]]
local n = 0
if _entities then
-- Manual pairs loop for performance
local k,v = next(_entities)
if ret ~= nil then
return ret
while k do
n = n + 1
local ret = fn(v, k)
if ret ~= nil then
return ret
end
k,v = next(_entities, k)
end
end
end
else
if not GlobalState.Entities then
local entities = UtilityNet.GetEntities()
if not entities then
return
end
for sliceI,slice in pairs(GlobalState.Entities) do
for k2, v in pairs(slice) do
-- Manual pairs loop for performance
local sliceI,slice = next(entities)
while sliceI do
local k2, v = next(slice)
while k2 do
local ret = fn(v, k2)
if ret ~= nil then
return ret
end
k2,v = next(slice, k2)
end
sliceI, slice = next(entities, sliceI)
end
end
end
@ -798,11 +840,7 @@ end
-- Returns the slice the entity is in
UtilityNet.InternalFindFromNetId = function(uNetId)
for sliceI, slice in pairs(GlobalState.Entities) do
if slice[uNetId] then
return slice[uNetId], sliceI
end
end
return exports["utility_lib"]:InternalFindFromNetId(uNetId)
end
UtilityNet.DoesUNetIdExist = function(uNetId)
@ -835,16 +873,20 @@ UtilityNet.GetEntityModel = function(uNetId)
end
end
UtilityNet.GetEntities = function()
return exports["utility_lib"]:GetEntities()
end
UtilityNet.SetModelRenderDistance = function(model, distance)
return exports["utility_lib"]:SetModelRenderDistance(model, distance)
end
UtilityNet.SetEntityCoords = function(uNetId, newCoords)
return exports["utility_lib"]:SetEntityCoords(uNetId, newCoords)
UtilityNet.SetEntityCoords = function(uNetId, newCoords, skipPositionUpdate)
return exports["utility_lib"]:SetEntityCoords(uNetId, newCoords, skipPositionUpdate)
end
UtilityNet.SetEntityRotation = function(uNetId, newRotation)
return exports["utility_lib"]:SetEntityRotation(uNetId, newRotation)
UtilityNet.SetEntityRotation = function(uNetId, newRotation, skipRotationUpdate)
return exports["utility_lib"]:SetEntityRotation(uNetId, newRotation, skipRotationUpdate)
end
UtilityNet.DetachEntity = function(uNetId)
@ -917,7 +959,25 @@ getValueAsStateTable = function(id, baseKey, depth)
})
end
UtilityNet.AddStateBagChangeHandler = function(uNetId, func)
return AddEventHandler("Utility:Net:UpdateStateValue", function(s_uNetId, key, value)
if uNetId == s_uNetId then
func(key, value)
end
end)
end
UtilityNet.RemoveStateBagChangeHandler = function(eventData)
if eventData and eventData.key and eventData.name then
RemoveEventHandler(eventData)
end
end
UtilityNet.State = function(id)
if not id then
error("UtilityNet.State: id is required, got nil", 2)
end
local state = setmetatable({
raw = function(self)
return exports["utility_lib"]:GetEntityStateValue(id)

View file

@ -1 +1 @@
1.2.0
1.3.7

View file

@ -1,4 +1,4 @@
local version = '1.2.0'
local version = '1.3.7'
local versionurl = "https://raw.githubusercontent.com/utility-library/utility_lib/master/version"
PerformHttpRequest(versionurl, function(error, _version, header)