Main/resources/[standalone]/utility_lib/client/functions/utilityNet.lua
2025-06-07 08:51:21 +02:00

505 lines
16 KiB
Lua

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)