From 640cdd069b8a5e8a78de289a6f1dd72d5b51dbde Mon Sep 17 00:00:00 2001 From: Nordi98 Date: Tue, 12 Aug 2025 16:56:50 +0200 Subject: [PATCH] red --- .../client/functions/utilityNet.lua | 389 +++++++++++++----- .../client/functions/utilityNet_states.lua | 62 ++- .../utility_lib/client/native.lua | 90 +++- .../server/functions/utilityNet.lua | 177 +++----- .../server/functions/utilityNet_states.lua | 15 + .../[standalone]/utility_lib/server/main.lua | 1 - .../utility_lib/server/native.lua | 114 +++-- resources/[standalone]/utility_lib/version | 2 +- .../utility_lib/version_checker.lua | 2 +- 9 files changed, 583 insertions(+), 269 deletions(-) diff --git a/resources/[standalone]/utility_lib/client/functions/utilityNet.lua b/resources/[standalone]/utility_lib/client/functions/utilityNet.lua index ba3ebbbe3..061312564 100644 --- a/resources/[standalone]/utility_lib/client/functions/utilityNet.lua +++ b/resources/[standalone]/utility_lib/client/functions/utilityNet.lua @@ -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) \ No newline at end of file diff --git a/resources/[standalone]/utility_lib/client/functions/utilityNet_states.lua b/resources/[standalone]/utility_lib/client/functions/utilityNet_states.lua index 2b1731e1c..230758825 100644 --- a/resources/[standalone]/utility_lib/client/functions/utilityNet_states.lua +++ b/resources/[standalone]/utility_lib/client/functions/utilityNet_states.lua @@ -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) diff --git a/resources/[standalone]/utility_lib/client/native.lua b/resources/[standalone]/utility_lib/client/native.lua index 896f20ece..42e9e2d41 100644 --- a/resources/[standalone]/utility_lib/client/native.lua +++ b/resources/[standalone]/utility_lib/client/native.lua @@ -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 diff --git a/resources/[standalone]/utility_lib/server/functions/utilityNet.lua b/resources/[standalone]/utility_lib/server/functions/utilityNet.lua index 04d848b86..88c97fc69 100644 --- a/resources/[standalone]/utility_lib/server/functions/utilityNet.lua +++ b/resources/[standalone]/utility_lib/server/functions/utilityNet.lua @@ -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) diff --git a/resources/[standalone]/utility_lib/server/functions/utilityNet_states.lua b/resources/[standalone]/utility_lib/server/functions/utilityNet_states.lua index 70b6335ac..4310b7b81 100644 --- a/resources/[standalone]/utility_lib/server/functions/utilityNet_states.lua +++ b/resources/[standalone]/utility_lib/server/functions/utilityNet_states.lua @@ -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) diff --git a/resources/[standalone]/utility_lib/server/main.lua b/resources/[standalone]/utility_lib/server/main.lua index ad7da574c..887db542a 100644 --- a/resources/[standalone]/utility_lib/server/main.lua +++ b/resources/[standalone]/utility_lib/server/main.lua @@ -1,5 +1,4 @@ GlobalState.ModelsRenderDistance = {} -GlobalState.Entities = {} LoadUtilityFrameworkIfFound() diff --git a/resources/[standalone]/utility_lib/server/native.lua b/resources/[standalone]/utility_lib/server/native.lua index 13aa5e2fa..179a36d5c 100644 --- a/resources/[standalone]/utility_lib/server/native.lua +++ b/resources/[standalone]/utility_lib/server/native.lua @@ -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) diff --git a/resources/[standalone]/utility_lib/version b/resources/[standalone]/utility_lib/version index 867e52437..8ed486ab7 100644 --- a/resources/[standalone]/utility_lib/version +++ b/resources/[standalone]/utility_lib/version @@ -1 +1 @@ -1.2.0 \ No newline at end of file +1.3.7 \ No newline at end of file diff --git a/resources/[standalone]/utility_lib/version_checker.lua b/resources/[standalone]/utility_lib/version_checker.lua index da4f76290..77a2712b5 100644 --- a/resources/[standalone]/utility_lib/version_checker.lua +++ b/resources/[standalone]/utility_lib/version_checker.lua @@ -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)