forked from Simnation/Main
505 lines
16 KiB
Lua
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)
|