local Entity = Entity local DebugRendering = false local DebugInfos = false local DeletedEntities = {} --#region Local functions local GetActiveSlices = function() local slices = GetSurroundingSlices(currentSlice) table.insert(slices, currentSlice) return slices end local AttachToEntity = function(obj, to, params) local attachToObj = nil if not params.isUtilityNet then attachToObj = NetworkGetEntityFromNetworkId(to) else -- Ensure that the entity is fully ready if UtilityNet.DoesUNetIdExist(to) then while not UtilityNet.IsReady(to) do Citizen.Wait(1) end else warn("AttachToEntity: trying to attach "..obj.." to "..to.." but the destination netId doesnt exist") end attachToObj = UtilityNet.GetEntityFromUNetId(to) end if attachToObj then if DebugInfos then print("Attaching", obj.." ("..GetEntityArchetypeName(obj)..")", "with", tostring(attachToObj).." ("..GetEntityArchetypeName(attachToObj)..")") end AttachEntityToEntity(obj, attachToObj, params.bone, params.pos, params.rot, false, params.useSoftPinning, params.collision, true, params.rotationOrder, params.syncRot) else warn("AttachToEntity: trying to attach "..obj.." to "..to.." but the destination entity doesnt exist") end end local FindEntity = function(coords, radius, model, uNetId, maxAttempts) local attempts = 0 local obj = 0 while attempts < maxAttempts and not DoesEntityExist(obj) do obj = GetClosestObjectOfType(coords.xyz, radius or 5.0, model) attempts = attempts + 1 Citizen.Wait(500) end if attempts >= maxAttempts and not DoesEntityExist(obj) then warn("Failed to find object to replace, model: "..model.." coords: "..coords.." uNetId:"..uNetId) return end return obj end --#endregion --#region Rendering functions local busyEntities = {} local SetNetIdBeingBusy = function(uNetId, status) busyEntities[uNetId] = status and true or nil end local IsNetIdBusy = function(uNetId) return busyEntities[uNetId] end exports("IsNetIdBusy", IsNetIdBusy) exports("IsNetIdCreating", IsNetIdBusy) local UnrenderLocalEntity = function(uNetId) local entity = UtilityNet.GetEntityFromUNetId(uNetId) if DoesEntityExist(entity) then TriggerEvent("Utility:Net:OnUnrender", uNetId, entity, GetEntityModel(entity)) Citizen.SetTimeout(1, function() if not DoesEntityExist(entity) then if DebugInfos then warn("UnrenderLocalEntity: entity with uNetId: "..uNetId.." already unrendered, skipping this call") 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 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 LocalEntities[uNetId] = nil end local RenderLocalEntity = function(uNetId, entityData) if IsNetIdBusy(uNetId) then if DebugRendering then warn("RenderLocalEntity: entity with uNetId: "..uNetId.." is already being created, skipping this call") end return end SetNetIdBeingBusy(uNetId, true) local obj = 0 local stateUtility = UtilityNet.State(uNetId) local entityData = entityData or UtilityNet.InternalFindFromNetId(uNetId) -- Exit if entity data is missing if not entityData then error("UpdateLocalEntity: entity with uNetId: "..tostring(uNetId).." cant be found") return false end -- Set local variable (for readability) local coords = entityData.coords 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) end 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 end Citizen.CreateThread(function() if options.replace then local _obj = FindEntity(coords, options.searchDistance, model, uNetId, 5) -- Skip object creation if not found if not DoesEntityExist(_obj) then SetNetIdBeingBusy(uNetId, false) return end -- Clone object (otherwise it will be deleted when the entity is unrendered and will not respawn properly) local coords = GetEntityCoords(_obj) local rotation = GetEntityRotation(_obj) local interior = GetInteriorFromEntity(_obj) local room = GetRoomKeyFromEntity(_obj) obj = CreateObject(model, coords, false, false, options.door) SetEntityCoords(obj, coords) SetEntityRotation(obj, rotation) if interior ~= 0 and room ~= 0 then ForceRoomForEntity(obj, interior, room) end Entity(obj).state.found = true -- Hide map object local distance = options.door and 1.5 or 0.1 if options.door and interior ~= 0 then -- 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) else CreateModelHideExcludingScriptObjects(coords, distance, model) end else obj = CreateObject(model, coords, false, false, options.door) SetEntityCoords(obj, coords) -- This is required to ignore the pivot end local state = Entity(obj).state -- "Disable" the entity SetEntityVisible(obj, false) SetEntityCollision(obj, false, false) if options.rotation then SetEntityRotation(obj, options.rotation) end -- Always listen for __attached changes (attach/detach) state.changeHandler = UtilityNet.AddStateBagChangeHandler(uNetId, function(key, value) -- Exit if entity is no longer valid if not DoesEntityExist(obj) then UtilityNet.RemoveStateBagChangeHandler(state.changeHandler) return end if key == "__attached" then if value then --print("Attach") AttachToEntity(obj, value.object, value.params) else --print("Detach") DetachEntity(obj, true, true) end end end) LocalEntities[uNetId] = {obj=obj, slice=entityData.slice} -- Fetch initial state ServerRequestEntityStates(uNetId) -- After state has been fetched, attach if needed if stateUtility.__attached then AttachToEntity(obj, stateUtility.__attached.object, stateUtility.__attached.params) end -- "Enable" the entity, this is done after the state has been fetched to avoid props doing strange stuffs SetEntityVisible(obj, true) SetEntityCollision(obj, true, true) TriggerEvent("Utility:Net:OnRender", uNetId, obj, model) state.rendered = true SetNetIdBeingBusy(uNetId, false) end) end local CanEntityBeRendered = function(uNetId, entityData, slices) -- Default values local entityData = entityData or UtilityNet.InternalFindFromNetId(uNetId) -- Exit if entity data is missing if not entityData then return false end -- Check if entity is within drawing slices (if provided) if slices and not 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 end return true end --#endregion StartUtilityNetRenderLoop = function() -- Wait for player full load local isLoading = false while not HasCollisionLoadedAroundEntity(player) or not NetworkIsPlayerActive(PlayerId()) do isLoading = true Citizen.Wait(100) end if isLoading then Citizen.Wait(1000) end Citizen.CreateThread(function() local lastNEntities = 0 -- Used for managing the speed of the loop based on the number of entities local lastSlice = currentSlice while true do DeletedEntities = {} local slices = GetActiveSlices() local start = GetGameTimer() local somethingRendered = false -- If something has been rendered, speed up the whole loop to avoid a ugly effect where everything loads slowly local nEntities = 0 local sleep = (Config.UtilityNetDynamicUpdate - 700) / math.min(20, lastNEntities) -- threshold to allow a little bit of lag and split by number of entities -- 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 {} if not state.rendered then somethingRendered = true if DebugRendering then print("RenderLocalEntity", v.id, "Loop") end RenderLocalEntity(v.id, v) end elseif LocalEntities[v.id] and not CanEntityBeRendered(v.id, v) then somethingRendered = true UnrenderLocalEntity(v.id) end local outOfTime = (GetGameTimer() - start) > Config.UtilityNetDynamicUpdate if not somethingRendered or outOfTime then Citizen.Wait(sleep * (2/3)) end end, slices) -- 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] if not CanEntityBeRendered(netId, entityData) then UnrenderLocalEntity(netId) end Citizen.Wait(sleep * (1/3)) end lastSlice = currentSlice end if DebugRendering then print("end", GetGameTimer() - start) end lastNEntities = nEntities Citizen.Wait(Config.UpdateCooldown) end end) end RegisterNetEvent("Utility:Net:RefreshModel", function(uNetId, model) local start = GetGameTimer() while not LocalEntities[uNetId] and (GetGameTimer() - start < 3000) do Citizen.Wait(1) end if LocalEntities[uNetId] then -- Wait for the entity to exist and be rendered (prevent missing model replace on instant model change) while not UtilityNet.IsReady(uNetId) or IsNetIdBusy(uNetId) do Citizen.Wait(100) end SetNetIdBeingBusy(uNetId, true) -- Preserve the old object so that it does not flash (delete and instantly re-render) local oldObj = LocalEntities[uNetId].obj local _state = Entity(oldObj).state _state.preserved = true UnrenderLocalEntity(uNetId) -- 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) local time = GetGameTimer() -- Wait for the entity to exist and be rendered while not UtilityNet.IsReady(uNetId) do if GetGameTimer() - time > 3000 then break end Citizen.Wait(1) end -- Delete the old object after the new one is rendered (so that it does not flash) DeleteEntity(oldObj) end end) RegisterNetEvent("Utility:Net:RefreshCoords", function(uNetId, coords) 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) SetEntityCoords(LocalEntities[uNetId].obj, coords) SetNetIdBeingBusy(uNetId, false) end end) 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") end return end Citizen.Wait(100) end if CanEntityBeRendered(uNetId) then if DebugRendering then print("RenderLocalEntity", uNetId, "EntityCreated") end RenderLocalEntity(uNetId) end end) RegisterNetEvent("Utility:Net:RequestDeletion", function(uNetId) if LocalEntities[uNetId] then DeletedEntities[uNetId] = true UnrenderLocalEntity(uNetId) end end) Citizen.CreateThread(function() while DebugRendering do DrawText3Ds(GetEntityCoords(PlayerPedId()), "Rendering Requested Entities: ".. #busyEntities) Citizen.Wait(1) end end) -- Exports UtilityNet.GetEntityFromUNetId = function(uNetId) return LocalEntities[uNetId]?.obj end UtilityNet.GetUNetIdFromEntity = function(entity) for k, v in pairs(LocalEntities) do if v.obj == entity then return k end end end exports("GetEntityFromUNetId", UtilityNet.GetEntityFromUNetId) exports("GetUNetIdFromEntity", UtilityNet.GetUNetIdFromEntity) exports("GetRenderedEntities", function() return LocalEntities end)