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 DebugRendering = false
local DebugInfos = 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 DeletedEntities = {}
local EntitiesLoaded = false
local Entities = {}
--#region Local functions --#region Local functions
local GetActiveSlices = function() local GetActiveSlices = function()
local slices = GetSurroundingSlices(currentSlice) local slices = GetSurroundingSlices(currentSlice)
@ -76,41 +82,58 @@ local UnrenderLocalEntity = function(uNetId)
local entity = UtilityNet.GetEntityFromUNetId(uNetId) local entity = UtilityNet.GetEntityFromUNetId(uNetId)
if DoesEntityExist(entity) then if DoesEntityExist(entity) then
TriggerEvent("Utility:Net:OnUnrender", uNetId, entity, GetEntityModel(entity)) local state = Entity(entity).state
Citizen.SetTimeout(1, function() if not state.preserved then
if not DoesEntityExist(entity) then TriggerEvent("Utility:Net:OnUnrender", uNetId, entity, GetEntityModel(entity))
if DebugInfos then end
warn("UnrenderLocalEntity: entity with uNetId: "..uNetId.." already unrendered, skipping this call")
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 end
return else
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) local model = GetEntityModel(entity)
-- Show map object -- Show map object
RemoveModelHide(GetEntityCoords(entity), 0.1, model) RemoveModelHide(GetEntityCoords(entity), 0.1, model)
end end
end
if state.preserved then
SetEntityAsNoLongerNeeded(entity) if not state.preserved then
else DeleteEntity(entity)
DeleteEntity(entity) end
end
state.rendered = false
state.rendered = false EntitiesStates[uNetId] = nil
EntitiesStates[uNetId] = nil TriggerLatentServerEvent("Utility:Net:RemoveStateListener", 5120, uNetId)
TriggerLatentServerEvent("Utility:Net:RemoveStateListener", 5120, uNetId)
end) 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 end
LocalEntities[uNetId] = nil LocalEntities[uNetId] = nil
@ -141,17 +164,44 @@ local RenderLocalEntity = function(uNetId, entityData)
local model = entityData.model local model = entityData.model
local options = entityData.options local options = entityData.options
if not options.replace then if options.abstract then
if not IsModelValid(model) then if options.replace then
error("RenderLocalEntity: Model "..model.." is not valid, uNetId: "..uNetId) error("RenderLocalEntity: abstract entities can't have the \"replace\" option, uNetId: "..uNetId.." model: "..model)
end 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() local start = GetGameTimer()
while not HasModelLoaded(model) do while not HasModelLoaded(model) do
if (GetGameTimer() - start) > 5000 then if (GetGameTimer() - start) > 5000 then
error("RenderLocalEntity: Model "..model.." failed to load, uNetId: "..uNetId) error("RenderLocalEntity: Model "..model.." failed to load, uNetId: "..uNetId)
end end
RequestModel(model) RequestModel(model)
Citizen.Wait(1) Citizen.Wait(1)
end end
@ -188,15 +238,23 @@ local RenderLocalEntity = function(uNetId, entityData)
local distance = options.door and 1.5 or 0.1 local distance = options.door and 1.5 or 0.1
if options.door and interior ~= 0 then if options.door and interior ~= 0 then
Entity(obj).state.door = _obj
-- Doors inside interiors need to be deleted -- 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) -- 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 -- And so there will be 2 copies of the same door
DeleteEntity(_obj) SetEntityVisible(_obj, false)
SetEntityCollision(_obj, false, false)
else else
CreateModelHideExcludingScriptObjects(coords, distance, model) CreateModelHideExcludingScriptObjects(coords, distance, model)
end end
else 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 SetEntityCoords(obj, coords) -- This is required to ignore the pivot
end end
@ -210,6 +268,10 @@ local RenderLocalEntity = function(uNetId, entityData)
SetEntityRotation(obj, options.rotation) SetEntityRotation(obj, options.rotation)
end end
if options.abstract then
Entity(obj).state.abstract_model = model
end
-- Always listen for __attached changes (attach/detach) -- Always listen for __attached changes (attach/detach)
state.changeHandler = UtilityNet.AddStateBagChangeHandler(uNetId, function(key, value) state.changeHandler = UtilityNet.AddStateBagChangeHandler(uNetId, function(key, value)
-- Exit if entity is no longer valid -- Exit if entity is no longer valid
@ -226,10 +288,12 @@ local RenderLocalEntity = function(uNetId, entityData)
--print("Detach") --print("Detach")
DetachEntity(obj, true, true) DetachEntity(obj, true, true)
end end
LocalEntities[uNetId].attached = value
end end
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 -- Fetch initial state
ServerRequestEntityStates(uNetId) ServerRequestEntityStates(uNetId)
@ -260,28 +324,26 @@ local CanEntityBeRendered = function(uNetId, entityData, slices)
end end
-- Check if entity is within drawing slices (if provided) -- 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 return false
end end
local state = UtilityNet.State(uNetId)
if DeletedEntities[uNetId] then if DeletedEntities[uNetId] then
return false return false
end end
-- Render only if within render distance -- Render only if within render distance
if not state.__attached then -- Skip distance check if entity is rendered and attached (keep them alive)
local coords = GetEntityCoords(PlayerPedId()) if LocalEntities[uNetId] and LocalEntities[uNetId].attached then
local modelsRenderDistance = GlobalState.ModelsRenderDistance return true
local renderDistance = modelsRenderDistance[entityData.model] or 50.0
if #(entityData.coords - coords) > renderDistance then
return false
end
end 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 end
--#endregion --#endregion
@ -315,7 +377,6 @@ StartUtilityNetRenderLoop = function()
-- Render/Unrender near slices entities -- Render/Unrender near slices entities
UtilityNet.ForEachEntity(function(v) UtilityNet.ForEachEntity(function(v)
nEntities = nEntities + 1 nEntities = nEntities + 1
if not LocalEntities[v.id] and CanEntityBeRendered(v.id, v) then if not LocalEntities[v.id] and CanEntityBeRendered(v.id, v) then
local obj = UtilityNet.GetEntityFromUNetId(v.id) or 0 local obj = UtilityNet.GetEntityFromUNetId(v.id) or 0
local state = Entity(obj).state or {} local state = Entity(obj).state or {}
@ -342,10 +403,8 @@ StartUtilityNetRenderLoop = function()
-- Unrender entities that are out of slice -- 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) -- Run only if the slice has changed (so something can be out of the slice and need to be unrendered)
if lastSlice ~= currentSlice then if lastSlice ~= currentSlice then
local entities = GlobalState.Entities
for netId, data in pairs(LocalEntities) do 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 if not CanEntityBeRendered(netId, entityData) then
UnrenderLocalEntity(netId) UnrenderLocalEntity(netId)
@ -368,8 +427,35 @@ StartUtilityNetRenderLoop = function()
end end
RegisterNetEvent("Utility:Net:RefreshModel", function(uNetId, model) RegisterNetEvent("Utility:Net:RefreshModel", function(uNetId, model)
local timeout = 3000
local start = GetGameTimer() 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) Citizen.Wait(1)
end end
@ -390,13 +476,6 @@ RegisterNetEvent("Utility:Net:RefreshModel", function(uNetId, model)
-- Tamper with the entity model and render again -- Tamper with the entity model and render again
local entityData = UtilityNet.InternalFindFromNetId(uNetId) 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) SetNetIdBeingBusy(uNetId, false)
RenderLocalEntity(uNetId, entityData) RenderLocalEntity(uNetId, entityData)
@ -415,68 +494,108 @@ RegisterNetEvent("Utility:Net:RefreshModel", function(uNetId, model)
end end
end) end)
RegisterNetEvent("Utility:Net:RefreshCoords", function(uNetId, coords) RegisterNetEvent("Utility:Net:RefreshCoords", function(uNetId, coords, skipPositionUpdate)
local start = GetGameTimer() local start = GetGameTimer()
while not LocalEntities[uNetId] and (GetGameTimer() - start < 3000) do local entity, slice = UtilityNet.InternalFindFromNetId(uNetId)
Citizen.Wait(1)
end
if LocalEntities[uNetId] then
while not UtilityNet.IsReady(uNetId) or IsNetIdBusy(uNetId) do
Citizen.Wait(100)
end
SetNetIdBeingBusy(uNetId, true) if entity and Entities[slice] then
SetEntityCoords(LocalEntities[uNetId].obj, coords) local newSlice = GetSliceFromCoords(coords)
SetNetIdBeingBusy(uNetId, false)
end
end)
RegisterNetEvent("Utility:Net:RefreshRotation", function(uNetId, rotation) if newSlice ~= slice then
local start = GetGameTimer() local entity = Entities[slice][uNetId]
while not LocalEntities[uNetId] and (GetGameTimer() - start < 3000) do Entities[slice][uNetId] = nil
Citizen.Wait(1)
end if not Entities[newSlice] then
Entities[newSlice] = {}
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 end
return
Entities[newSlice][uNetId] = entity
slice = newSlice
end end
Citizen.Wait(100)
Entities[slice][uNetId].coords = coords
Entities[slice][uNetId].slice = newSlice
end 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 if DebugRendering then
print("RenderLocalEntity", uNetId, "EntityCreated") print("RenderLocalEntity", uNetId, "EntityCreated")
end end
RenderLocalEntity(uNetId) RenderLocalEntity(uNetId, object)
end end
end) end)
RegisterNetEvent("Utility:Net:RequestDeletion", function(uNetId) 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 if LocalEntities[uNetId] then
DeletedEntities[uNetId] = true DeletedEntities[uNetId] = true
UnrenderLocalEntity(uNetId) UnrenderLocalEntity(uNetId)
if Entities[slice] then
Entities[slice][uNetId] = nil
end
else
if Entities[slice] then
Entities[slice][uNetId] = nil
end
end end
end) end)
@ -487,6 +606,39 @@ Citizen.CreateThread(function()
end end
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 -- Exports
UtilityNet.GetEntityFromUNetId = function(uNetId) UtilityNet.GetEntityFromUNetId = function(uNetId)
return LocalEntities[uNetId]?.obj return LocalEntities[uNetId]?.obj
@ -500,6 +652,37 @@ UtilityNet.GetUNetIdFromEntity = function(entity)
end end
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("GetEntityFromUNetId", UtilityNet.GetEntityFromUNetId)
exports("GetUNetIdFromEntity", UtilityNet.GetUNetIdFromEntity) exports("GetUNetIdFromEntity", UtilityNet.GetUNetIdFromEntity)
exports("GetuNetIdCreator", UtilityNet.GetuNetIdCreator)
exports("GetEntityCreator", UtilityNet.GetEntityCreator)
exports("GetRenderedEntities", function() return LocalEntities end) 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 = {} 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) RegisterNetEvent("Utility:Net:UpdateStateValue", function(uNetId, key, value)
EnsureStateLoaded(uNetId)
if not EntitiesStates[uNetId] then if not EntitiesStates[uNetId] then
EntitiesStates[uNetId] = {} EntitiesStates[uNetId] = {}
end end
@ -9,14 +28,49 @@ RegisterNetEvent("Utility:Net:UpdateStateValue", function(uNetId, key, value)
end) end)
GetEntityStateValue = function(uNetId, key) GetEntityStateValue = function(uNetId, key)
if not EntitiesStates[uNetId] then if not UtilityNet.GetEntityFromUNetId(uNetId) then -- If trying to get state of entity that isnt loaded
return 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 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 end
ServerRequestEntityStates = function(uNetId) ServerRequestEntityStates = function(uNetId)
EntitiesStates[uNetId] = -1 -- Set as loading
local p = promise:new() local p = promise:new()
local event = nil local event = nil
@ -28,7 +82,7 @@ ServerRequestEntityStates = function(uNetId)
TriggerServerEvent("Utility:Net:GetState", uNetId) TriggerServerEvent("Utility:Net:GetState", uNetId)
local states = Citizen.Await(p) local states = Citizen.Await(p)
EntitiesStates[uNetId] = states EntitiesStates[uNetId] = states or {}
end end
exports("GetEntityStateValue", GetEntityStateValue) exports("GetEntityStateValue", GetEntityStateValue)

View file

@ -1447,6 +1447,28 @@ end
return values return values
end 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) math.round = function(number, decimals)
local _ = 10 ^ decimals local _ = 10 ^ decimals
@ -2512,7 +2534,8 @@ end
NetworkRequestControlOfEntity(trolly) NetworkRequestControlOfEntity(trolly)
end 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) SetEntityCollision(bagObj, false, true)
-- Intro -- Intro
@ -2984,13 +3007,26 @@ end
--// UtilityNet // --// UtilityNet //
local CreatedEntities = {} 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 --#region API
UtilityNet.ForEachEntity = function(fn, slices) UtilityNet.ForEachEntity = function(fn, slices)
if slices then if slices then
local entities = GlobalState.Entities
for i = 1, #slices do for i = 1, #slices do
local _entities = entities[slices[i]] local _entities = UtilityNet.GetEntities(slices[i])
local n = 0 local n = 0
if _entities then if _entities then
@ -3009,7 +3045,7 @@ UtilityNet.ForEachEntity = function(fn, slices)
end end
end end
else else
local entities = GlobalState.Entities local entities = UtilityNet.GetEntities()
if not entities then if not entities then
return return
@ -3035,14 +3071,6 @@ UtilityNet.ForEachEntity = function(fn, slices)
end end
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) UtilityNet.SetDebug = function(state)
UtilityNetDebug = state UtilityNetDebug = state
@ -3071,7 +3099,9 @@ UtilityNet.SetDebug = function(state)
for k,v in pairs(localEntities) do for k,v in pairs(localEntities) do
local state = UtilityNet.State(v.netId) 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 end
Citizen.Wait(1) Citizen.Wait(1)
end end
@ -3089,16 +3119,16 @@ UtilityNet.CreateEntity = function(model, coords, options)
-- Set resource name in options -- Set resource name in options
options = options or {} options = options or {}
options.resource = GetCurrentResourceName() options.createdBy = GetCurrentResourceName()
local callId = math.random(0, 10000000) local callId = math.random(0, 10000000)
local event = nil local event = nil
local entity = promise:new() local entity = promise:new()
-- Callback -- Callback
event = RegisterNetEvent("Utility:Net:EntityCreated", function(_callId, uNetId) event = RegisterNetEvent("Utility:Net:EntityCreated", function(_callId, object)
if _callId == callId then if _callId == callId then
entity:resolve(uNetId) entity:resolve(object.id)
RemoveEventHandler(event) RemoveEventHandler(event)
end end
end) end)
@ -3207,18 +3237,18 @@ UtilityNet.DetachEntity = function(uNetId)
end end
-- Using a latent event to prevent blocking the network channel -- 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) TriggerLatentServerEvent("Utility:Net:SetEntityCoords", 5120, uNetId, coords)
-- Instantly sync for local obj -- Instantly sync for local obj
TriggerEvent("Utility:Net:RefreshCoords", uNetId, coords) TriggerEvent("Utility:Net:RefreshCoords", uNetId, coords, skipPositionUpdate)
end end
UtilityNet.SetEntityRotation = function(uNetId, rot) UtilityNet.SetEntityRotation = function(uNetId, rot, skipRotationUpdate)
TriggerLatentServerEvent("Utility:Net:SetEntityRotation", 5120, uNetId, rot) TriggerLatentServerEvent("Utility:Net:SetEntityRotation", 5120, uNetId, rot, skipRotationUpdate)
-- Instantly sync for local obj -- Instantly sync for local obj
TriggerEvent("Utility:Net:RefreshRotation", uNetId, rot) TriggerEvent("Utility:Net:RefreshRotation", uNetId, rot, skipRotationUpdate)
end end
UtilityNet.SetEntityModel = function(uNetId, model) UtilityNet.SetEntityModel = function(uNetId, model)
@ -3322,6 +3352,22 @@ end
UtilityNet.GetUNetIdFromEntity = function(entity) UtilityNet.GetUNetIdFromEntity = function(entity)
return exports["utility_lib"]:GetUNetIdFromEntity(entity) return exports["utility_lib"]:GetUNetIdFromEntity(entity)
end 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
--#endregion --#endregion

View file

@ -1,4 +1,5 @@
local NextId = 1 local NextId = 1
local Entities = {}
UtilityNet = UtilityNet or {} UtilityNet = UtilityNet or {}
@ -10,24 +11,27 @@ UtilityNet = UtilityNet or {}
-- } -- }
UtilityNet.CreateEntity = function(model, coords, options, callId) UtilityNet.CreateEntity = function(model, coords, options, callId)
options = options or {}
local hashmodel = nil
--#region Checks --#region Checks
if not model or (type(model) ~= "string" and type(model) ~= "number") then if not model or (type(model) ~= "string" and type(model) ~= "number") then
error("Invalid model, got "..type(model).." expected string or number", 0) error("Invalid model, got "..type(model).." expected string or number", 0)
else else
if type(model) == "string" then if type(model) == "string" then
model = GetHashKey(model) hashmodel = GetHashKey(model)
else
hashmodel = model
end end
end end
if not coords or type(coords) ~= "vector3" then if not coords or type(coords) ~= "vector3" then
error("Invalid coords, got "..type(coords).." expected vector3", 0) error("Invalid coords, got "..type(coords).." expected vector3", 0)
end end
options = options or {}
--#endregion --#endregion
--#region Event --#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 -- EntityCreating event can be canceled, in that case we dont create the entity
if WasEventCanceled() then if WasEventCanceled() then
@ -35,29 +39,27 @@ UtilityNet.CreateEntity = function(model, coords, options, callId)
end end
--#endregion --#endregion
local entities = GlobalState.Entities
local slice = GetSliceFromCoords(coords) local slice = GetSliceFromCoords(coords)
local object = { local object = {
id = NextId, id = NextId,
model = model, model = options.abstract and model or hashmodel,
coords = coords, coords = coords,
slice = slice, slice = slice,
options = options, options = options,
createdBy = options.resource or GetInvokingResource(), createdBy = options.createdBy or GetInvokingResource(),
} }
if not entities[slice] then if not Entities[slice] then
entities[slice] = {} Entities[slice] = {}
end end
entities[slice][object.id] = object Entities[slice][object.id] = object
GlobalState.Entities = entities
RegisterEntityState(object.id) RegisterEntityState(object.id)
NextId = NextId + 1 NextId = NextId + 1
TriggerLatentClientEvent("Utility:Net:EntityCreated", -1, 5120, callId, object.id) TriggerLatentClientEvent("Utility:Net:EntityCreated", -1, 5120, callId, object)
return object.id return object.id
end end
@ -85,103 +87,32 @@ UtilityNet.DeleteEntity = function(uNetId)
--#endregion --#endregion
local entities = GlobalState.Entities
local entity = UtilityNet.InternalFindFromNetId(uNetId) local entity = UtilityNet.InternalFindFromNetId(uNetId)
if entity then if entity then
entities[entity.slice][entity.id] = nil Entities[entity.slice][entity.id] = nil
end end
GlobalState.Entities = entities TriggerLatentClientEvent("Utility:Net:RequestDeletion", -1, 5120, uNetId)
TriggerLatentEventForListeners("Utility:Net:RequestDeletion", uNetId, 5120, uNetId)
ClearEntityStates(uNetId) -- Clear states after trigger ClearEntityStates(uNetId) -- Clear states after trigger
end end
local queues = { UtilityNet.InternalFindFromNetId = function(uNetId)
ModelsRenderDistance = {}, for sliceI, slice in pairs(Entities) do
Entities = {}, if slice[uNetId] then
} return slice[uNetId], sliceI
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)
end end
end) end
end end
local function InsertValueInQueue(bagkey, id, value) UtilityNet.GetEntities = function(slice)
-- If it is already in the queue with some values that need to be updated, we merge the 2 updates into 1 if slice then
if queues[bagkey][id] then return Entities[slice]
queues[bagkey][id] = table.merge(queues[bagkey][id], value)
else else
queues[bagkey][id] = value return Entities
end
queues[bagkey].lastInt = GetGameTimer()
if not queues[bagkey].updateLoop then
queues[bagkey].updateLoop = true
StartQueueUpdateLoop(bagkey)
end end
end end
UtilityNet.SetModelRenderDistance = function(model, distance) UtilityNet.SetModelRenderDistance = function(model, distance)
if type(model) == "string" then if type(model) == "string" then
model = GetHashKey(model) model = GetHashKey(model)
@ -192,30 +123,57 @@ UtilityNet.SetModelRenderDistance = function(model, distance)
GlobalState.ModelsRenderDistance = _ GlobalState.ModelsRenderDistance = _
end end
UtilityNet.SetEntityRotation = function(uNetId, newRotation) UtilityNet.SetEntityRotation = function(uNetId, newRotation, skipRotationUpdate)
local source = source local source = source
if type(newRotation) ~= "vector3" then if type(newRotation) ~= "vector3" then
error("Invalid rotation, got "..type(newRotation).." expected vector3", 2) error("Invalid rotation, got "..type(newRotation).." expected vector3", 2)
end 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 -- 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 end
UtilityNet.SetEntityCoords = function(uNetId, newCoords) UtilityNet.SetEntityCoords = function(uNetId, newCoords, skipPositionUpdate)
local source = source local source = source
if type(newCoords) ~= "vector3" then if type(newCoords) ~= "vector3" then
error("Invalid coords, got "..type(newCoords).." expected vector3", 2) error("Invalid coords, got "..type(newCoords).." expected vector3", 2)
end 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 -- 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 end
UtilityNet.SetEntityModel = function(uNetId, model) UtilityNet.SetEntityModel = function(uNetId, model)
@ -229,10 +187,12 @@ UtilityNet.SetEntityModel = function(uNetId, model)
model = GetHashKey(model) model = GetHashKey(model)
end 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 -- 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 end
--#region Events --#region Events
@ -244,7 +204,7 @@ UtilityNet.RegisterEvents = function()
RegisterNetEvent("Utility:Net:DeleteEntity", function(uNetId) RegisterNetEvent("Utility:Net:DeleteEntity", function(uNetId)
UtilityNet.DeleteEntity(uNetId) UtilityNet.DeleteEntity(uNetId)
end) end)
RegisterNetEvent("Utility:Net:SetModelRenderDistance", function(model, distance) RegisterNetEvent("Utility:Net:SetModelRenderDistance", function(model, distance)
UtilityNet.SetModelRenderDistance(model, distance) UtilityNet.SetModelRenderDistance(model, distance)
end) end)
@ -274,13 +234,8 @@ UtilityNet.RegisterEvents = function()
RegisterNetEvent("Utility:Net:SetEntityModel", UtilityNet.SetEntityModel) RegisterNetEvent("Utility:Net:SetEntityModel", UtilityNet.SetEntityModel)
RegisterNetEvent("Utility:Net:SetEntityRotation", UtilityNet.SetEntityRotation) RegisterNetEvent("Utility:Net:SetEntityRotation", UtilityNet.SetEntityRotation)
-- Clear all entities on resource stop RegisterNetEvent("Utility:Net:GetEntities", function()
AddEventHandler("onResourceStop", function(resource) TriggerClientEvent("Utility:Net:GetEntities", source, UtilityNet.GetEntities())
if resource == GetCurrentResourceName() then
UtilityNet.ForEachEntity(function(v)
TriggerLatentEventForListeners("Utility:Net:RequestDeletion", v, 5120, v)
end)
end
end) end)
end end
--#endregion --#endregion
@ -289,6 +244,8 @@ end
exports("CreateEntity", UtilityNet.CreateEntity) exports("CreateEntity", UtilityNet.CreateEntity)
exports("DeleteEntity", UtilityNet.DeleteEntity) exports("DeleteEntity", UtilityNet.DeleteEntity)
exports("SetModelRenderDistance", UtilityNet.SetModelRenderDistance) exports("SetModelRenderDistance", UtilityNet.SetModelRenderDistance)
exports("GetEntities", UtilityNet.GetEntities)
exports("InternalFindFromNetId", UtilityNet.InternalFindFromNetId)
exports("SetEntityModel", UtilityNet.SetEntityModel) exports("SetEntityModel", UtilityNet.SetEntityModel)
exports("SetEntityCoords", UtilityNet.SetEntityCoords) exports("SetEntityCoords", UtilityNet.SetEntityCoords)

View file

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

View file

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

View file

@ -30,23 +30,23 @@
--// Player //-- --// Player //--
-- Item -- Item
AddItem = function(source, ...) AddItem = function(source, item, amount, metadata, slot)
if ESX then if ESX then
xPlayer = ESX.GetPlayerFromId(source) xPlayer = ESX.GetPlayerFromId(source)
xPlayer.addInventoryItem(...) xPlayer.addInventoryItem(item, amount, metadata, slot)
else else
xPlayer = QBCore.Functions.GetPlayer(source) xPlayer = QBCore.Functions.GetPlayer(source)
xPlayer.Functions.AddItem(...) xPlayer.Functions.AddItem(item, amount, slot, metadata)
end end
end end
RemoveItem = function(source, ...) RemoveItem = function(source, item, amount, metadata, slot)
if ESX then if ESX then
xPlayer = ESX.GetPlayerFromId(source) xPlayer = ESX.GetPlayerFromId(source)
xPlayer.removeInventoryItem(...) xPlayer.removeInventoryItem(item, amount, metadata, slot)
else else
xPlayer = QBCore.Functions.GetPlayer(source) xPlayer = QBCore.Functions.GetPlayer(source)
xPlayer.Functions.RemoveItem(...) xPlayer.Functions.RemoveItem(item, amount, slot, metadata)
end end
end end
@ -616,6 +616,28 @@
return values return values
end 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) math.round = function(number, decimals)
local _ = 10 ^ decimals local _ = 10 ^ decimals
return math.floor((number * _) + 0.5) / (_) return math.floor((number * _) + 0.5) / (_)
@ -744,32 +766,52 @@
local CreatedEntities = {} local CreatedEntities = {}
UtilityNet = {} UtilityNet = {}
UtilityNet.ForEachEntity = function(fn, slice) UtilityNet.ForEachEntity = function(fn, slices)
if slice then if slices then
if not GlobalState.Entities[slice] then local entities = UtilityNet.GetEntities(slices)
return
end
for k,v in pairs(GlobalState.Entities[slice]) do for i = 1, #slices do
local ret = fn(v, k) 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 while k do
return ret n = n + 1
local ret = fn(v, k)
if ret ~= nil then
return ret
end
k,v = next(_entities, k)
end
end end
end end
else else
if not GlobalState.Entities then local entities = UtilityNet.GetEntities()
if not entities then
return return
end end
for sliceI,slice in pairs(GlobalState.Entities) do -- Manual pairs loop for performance
for k2, v in pairs(slice) do local sliceI,slice = next(entities)
while sliceI do
local k2, v = next(slice)
while k2 do
local ret = fn(v, k2) local ret = fn(v, k2)
if ret ~= nil then if ret ~= nil then
return ret return ret
end end
k2,v = next(slice, k2)
end end
sliceI, slice = next(entities, sliceI)
end end
end end
end end
@ -798,11 +840,7 @@ end
-- Returns the slice the entity is in -- Returns the slice the entity is in
UtilityNet.InternalFindFromNetId = function(uNetId) UtilityNet.InternalFindFromNetId = function(uNetId)
for sliceI, slice in pairs(GlobalState.Entities) do return exports["utility_lib"]:InternalFindFromNetId(uNetId)
if slice[uNetId] then
return slice[uNetId], sliceI
end
end
end end
UtilityNet.DoesUNetIdExist = function(uNetId) UtilityNet.DoesUNetIdExist = function(uNetId)
@ -835,16 +873,20 @@ UtilityNet.GetEntityModel = function(uNetId)
end end
end end
UtilityNet.GetEntities = function()
return exports["utility_lib"]:GetEntities()
end
UtilityNet.SetModelRenderDistance = function(model, distance) UtilityNet.SetModelRenderDistance = function(model, distance)
return exports["utility_lib"]:SetModelRenderDistance(model, distance) return exports["utility_lib"]:SetModelRenderDistance(model, distance)
end end
UtilityNet.SetEntityCoords = function(uNetId, newCoords) UtilityNet.SetEntityCoords = function(uNetId, newCoords, skipPositionUpdate)
return exports["utility_lib"]:SetEntityCoords(uNetId, newCoords) return exports["utility_lib"]:SetEntityCoords(uNetId, newCoords, skipPositionUpdate)
end end
UtilityNet.SetEntityRotation = function(uNetId, newRotation) UtilityNet.SetEntityRotation = function(uNetId, newRotation, skipRotationUpdate)
return exports["utility_lib"]:SetEntityRotation(uNetId, newRotation) return exports["utility_lib"]:SetEntityRotation(uNetId, newRotation, skipRotationUpdate)
end end
UtilityNet.DetachEntity = function(uNetId) UtilityNet.DetachEntity = function(uNetId)
@ -917,7 +959,25 @@ getValueAsStateTable = function(id, baseKey, depth)
}) })
end 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) UtilityNet.State = function(id)
if not id then
error("UtilityNet.State: id is required, got nil", 2)
end
local state = setmetatable({ local state = setmetatable({
raw = function(self) raw = function(self)
return exports["utility_lib"]:GetEntityStateValue(id) 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" local versionurl = "https://raw.githubusercontent.com/utility-library/utility_lib/master/version"
PerformHttpRequest(versionurl, function(error, _version, header) PerformHttpRequest(versionurl, function(error, _version, header)