Merge pull request #1801 from esx-framework/1.14.0

feat(esx_lib): upgrade esx_core to 1.14.0
This commit is contained in:
_Not_
2026-07-17 08:16:34 -05:00
committed by GitHub
46 changed files with 3674 additions and 1457 deletions
+90
View File
@@ -0,0 +1,90 @@
--All client-side functions outsourced from the Core to the lib will be stored here for compatability, e.g:
ESX.Game.GetClosestEntity = xLib.entity.closest
EnumerateEntitiesWithinDistance = xLib.entity.EnumerateWithinDistance
ESX.Game.Teleport = xLib.entity.Teleport
ESX.Streaming = {
RequestModel = xLib.streaming.requestModel,
RequestStreamedTextureDict = xLib.streaming.requestStreamedTextureDict,
RequestNamedPtfxAsset = xLib.streaming.requestNamedPtfxAsset,
RequestAnimSet = xLib.streaming.requestAnimSet,
RequestAnimDict = xLib.streaming.requestAnimDict,
RequestWeaponAsset = xLib.streaming.requestWeaponAsset,
}
ESX.Scaleform = {
ShowFreemodeMessage = xLib.scaleform.showFreemodeMessage,
ShowBreakingNews = xLib.scaleform.showBreakingNews,
ShowPopupWarning = xLib.scaleform.showPopupWarning,
ShowTrafficMovie = xLib.scaleform.showTrafficMovie,
Utils = {
RequestScaleformMovie = xLib.scaleform.utils.requestScaleformMovie,
RunMethod = xLib.scaleform.utils.runMethod,
},
}
ESX.CreatePointInternal = xLib.points.create
ESX.RemovePointInternal = xLib.points.remove
ESX.HidePointInternal = xLib.points.hide
StartPointsLoop = xLib.points.startLoop
ESX.Point = xLib.point
ESX.RegisterInteraction = xLib.interactions.register
ESX.RemoveInteraction = xLib.interactions.remove
ESX.GetInteractKey = xLib.interactions.getInteractKey
ESX.Game.GetPedMugshot = xLib.game.getPedMugshot
ESX.Game.SpawnObject = xLib.game.spawnObject
ESX.Game.SpawnLocalObject = xLib.game.spawnLocalObject
ESX.Game.DeleteVehicle = xLib.game.deleteVehicle
ESX.Game.DeleteObject = xLib.game.deleteObject
ESX.Game.SpawnVehicle = xLib.game.spawnVehicle
ESX.Game.SpawnLocalVehicle = xLib.game.spawnLocalVehicle
ESX.Game.IsVehicleEmpty = xLib.game.isVehicleEmpty
ESX.Game.GetObjects = xLib.game.getObjects
ESX.Game.GetPeds = xLib.game.getPeds
ESX.Game.GetVehicles = xLib.game.getVehicles
ESX.Game.GetPlayers = xLib.game.getPlayers
ESX.Game.GetClosestObject = xLib.game.getClosestObject
ESX.Game.GetClosestPed = xLib.game.getClosestPed
ESX.Game.GetClosestPlayer = xLib.game.getClosestPlayer
ESX.Game.GetClosestVehicle = xLib.game.getClosestVehicle
ESX.Game.GetPlayersInArea = xLib.game.getPlayersInArea
ESX.Game.GetVehiclesInArea = xLib.game.getVehiclesInArea
ESX.Game.IsSpawnPointClear = xLib.game.isSpawnPointClear
ESX.Game.GetVehicleInDirection = xLib.game.getVehicleInDirection
ESX.Game.GetVehicleProperties = xLib.game.getVehicleProperties
ESX.Game.SetVehicleProperties = xLib.game.setVehicleProperties
function ESX.RegisterInput(command_name, label, input_group, key, on_press, on_release)
return xLib.addKeybind({
name = command_name,
description = label,
defaultMapper = input_group,
defaultKey = key,
onPressed = on_press,
onReleased = on_release
})
end
---@param raycast table The raycast object returned from ESX.Game.StartRaycasting
ESX.Game.StopRaycasting = function(raycast)
if raycast and raycast.active then
raycast:Stop()
end
end
---@param raycast table The raycast object returned from ESX.Game.StartRaycasting
ESX.Game.IsRaycastActive = function(raycast)
if raycast and raycast.active then
return raycast:IsActive()
end
return false
end
---@param raycast table The raycast object returned from ESX.Game.StartRaycasting
ESX.Game.GetRaycastResult = function(raycast)
if raycast and raycast.active then
return raycast.result
end
return nil
end
-789
View File
@@ -246,22 +246,6 @@ function ESX.RefreshContext(...)
return IsResourceFound('esx_context') and exports['esx_context']:Refresh(...)
end
---@param command_name string The command name
---@param label string The label to show
---@param input_group string The input group
---@param key string The key to bind
---@param on_press function The function to call on press
---@param on_release? function The function to call on release
function ESX.RegisterInput(command_name, label, input_group, key, on_press, on_release)
local command = on_release and '+' .. command_name or command_name
RegisterCommand(command, on_press, false)
Core.Input[command_name] = ESX.HashString(command)
if on_release then
RegisterCommand('-' .. command_name, on_release, false)
end
RegisterKeyMapping(command, label or '', input_group or 'keyboard', key or '')
end
---@param menuType string
---@param open function The function to call on open
---@param close function The function to call on close
@@ -459,779 +443,6 @@ function ESX.UI.ShowInventoryItemNotification(add, item, count)
})
end
---@param ped integer The ped to get the mugshot of
---@param transparent? boolean Whether the mugshot should be transparent
function ESX.Game.GetPedMugshot(ped, transparent)
if not DoesEntityExist(ped) then
return
end
local mugshot = transparent and RegisterPedheadshotTransparent(ped) or RegisterPedheadshot(ped)
while not IsPedheadshotReady(mugshot) do
Wait(0)
end
return mugshot, GetPedheadshotTxdString(mugshot)
end
---@param entity integer The entity to get the coords of
---@param coords table | vector3 | vector4 The coords to teleport the entity to
---@param cb? function The callback function
function ESX.Game.Teleport(entity, coords, cb)
if DoesEntityExist(entity) then
RequestCollisionAtCoord(coords.x, coords.y, coords.z)
while not HasCollisionLoadedAroundEntity(entity) do
Wait(0)
end
SetEntityCoords(entity, coords.x, coords.y, coords.z, false, false, false, false)
SetEntityHeading(entity, coords.w or coords.heading or 0.0)
end
if cb then
cb()
end
end
---@param object integer | string The object to spawn
---@param coords table | vector3 The coords to spawn the object at
---@param cb? function The callback function
---@param networked? boolean Whether the object should be networked
---@return integer | nil
function ESX.Game.SpawnObject(object, coords, cb, networked)
local model = type(object) == "number" and object or joaat(object)
ESX.Streaming.RequestModel(model)
local obj = CreateObject(model, coords.x, coords.y, coords.z, networked == nil or networked, false, true)
return cb and cb(obj) or obj
end
---@param object integer | string The object to spawn
---@param coords table | vector3 The coords to spawn the object at
---@param cb? function The callback function
---@return nil
function ESX.Game.SpawnLocalObject(object, coords, cb)
ESX.Game.SpawnObject(object, coords, cb, false)
end
---@param vehicle integer The vehicle to delete
---@return nil
function ESX.Game.DeleteVehicle(vehicle)
SetEntityAsMissionEntity(vehicle, true, true)
DeleteVehicle(vehicle)
end
---@param object integer The object to delete
---@return nil
function ESX.Game.DeleteObject(object)
SetEntityAsMissionEntity(object, false, true)
DeleteObject(object)
end
---@param vehicleModel integer | string The vehicle to spawn
---@param coords table | vector3 The coords to spawn the vehicle at
---@param heading number The heading of the vehicle
---@param cb? fun(vehicle: number) The callback function
---@param networked? boolean Whether the vehicle should be networked
---@return number? vehicle
function ESX.Game.SpawnVehicle(vehicleModel, coords, heading, cb, networked)
if cb and not ESX.IsFunctionReference(cb) then
error("Invalid callback function")
end
local model = type(vehicleModel) == "number" and vehicleModel or joaat(vehicleModel)
local vector = type(coords) == "vector3" and coords or vec(coords.x, coords.y, coords.z)
local isNetworked = networked == nil or networked
local playerCoords = GetEntityCoords(ESX.PlayerData.ped)
if not vector or not playerCoords then
return
end
local dist = #(playerCoords - vector)
if dist > 424 then -- Onesync infinity Range (https://docs.fivem.net/docs/scripting-reference/onesync/)
local executingResource = GetInvokingResource() or "Unknown"
return error(("Resource ^5%s^1 Tried to spawn vehicle on the client but the position is too far away (Out of onesync range)."):format(executingResource))
end
local promise = not cb and promise.new()
CreateThread(function()
local modelHash = ESX.Streaming.RequestModel(model)
if not modelHash then
if promise then
promise:reject(("Tried to spawn invalid vehicle - ^5%s^7!"):format(model))
return
end
error(("Tried to spawn invalid vehicle - ^5%s^7!"):format(model))
end
local vehicle = CreateVehicle(model, vector.x, vector.y, vector.z, heading, isNetworked, true)
if networked then
local id = NetworkGetNetworkIdFromEntity(vehicle)
SetNetworkIdCanMigrate(id, true)
SetEntityAsMissionEntity(vehicle, true, true)
end
SetVehicleHasBeenOwnedByPlayer(vehicle, true)
SetVehicleNeedsToBeHotwired(vehicle, false)
SetModelAsNoLongerNeeded(model)
SetVehRadioStation(vehicle, "OFF")
RequestCollisionAtCoord(vector.x, vector.y, vector.z)
while not HasCollisionLoadedAroundEntity(vehicle) do
Wait(0)
end
if promise then
promise:resolve(vehicle)
elseif cb then
cb(vehicle)
end
end)
if promise then
return Citizen.Await(promise)
end
end
---@param vehicle integer The vehicle to spawn
---@param coords table | vector3 The coords to spawn the vehicle at
---@param heading number The heading of the vehicle
---@param cb? function The callback function
---@return nil
function ESX.Game.SpawnLocalVehicle(vehicle, coords, heading, cb)
ESX.Game.SpawnVehicle(vehicle, coords, heading, cb, false)
end
---@param vehicle integer The vehicle to check
---@return boolean
function ESX.Game.IsVehicleEmpty(vehicle)
return GetVehicleNumberOfPassengers(vehicle) == 0 and IsVehicleSeatFree(vehicle, -1)
end
---@return table
function ESX.Game.GetObjects() -- Leave the function for compatibility
return GetGamePool("CObject")
end
---@param onlyOtherPeds? boolean Whether to exlude the player ped
---@return table
function ESX.Game.GetPeds(onlyOtherPeds)
local pool = GetGamePool("CPed")
if onlyOtherPeds then
local myPed = ESX.PlayerData.ped
for i = 1, #pool do
if pool[i] == myPed then
table.remove(pool, i)
break
end
end
end
return pool
end
---@return table
function ESX.Game.GetVehicles() -- Leave the function for compatibility
return GetGamePool("CVehicle")
end
---@param onlyOtherPlayers? boolean Whether to exclude the player
---@param returnKeyValue? boolean Whether to return the key value pair
---@param returnPeds? boolean Whether to return the peds
---@return table
function ESX.Game.GetPlayers(onlyOtherPlayers, returnKeyValue, returnPeds)
local players = {}
local active = GetActivePlayers()
for i = 1, #active do
local currentPlayer = active[i]
local ped = GetPlayerPed(currentPlayer)
if DoesEntityExist(ped) and ((onlyOtherPlayers and currentPlayer ~= ESX.playerId) or not onlyOtherPlayers) then
if returnKeyValue then
players[currentPlayer] = ped
else
players[#players + 1] = returnPeds and ped or currentPlayer
end
end
end
return players
end
---@param coords? table | vector3 The coords to get the closest object to
---@param modelFilter? table The model filter
---@return integer, integer
function ESX.Game.GetClosestObject(coords, modelFilter)
return ESX.Game.GetClosestEntity(ESX.Game.GetObjects(), false, coords, modelFilter)
end
---@param coords? table | vector3 The coords to get the closest ped to
---@param modelFilter? table The model filter
---@return integer, integer
function ESX.Game.GetClosestPed(coords, modelFilter)
return ESX.Game.GetClosestEntity(ESX.Game.GetPeds(true), false, coords, modelFilter)
end
---@param coords? table | vector3 The coords to get the closest player to
---@return integer, integer
function ESX.Game.GetClosestPlayer(coords)
return ESX.Game.GetClosestEntity(ESX.Game.GetPlayers(true, true), true, coords, nil)
end
---@param coords? table | vector3 The coords to get the closest vehicle to
---@param modelFilter? table The model filter
---@return integer, integer
function ESX.Game.GetClosestVehicle(coords, modelFilter)
return ESX.Game.GetClosestEntity(ESX.Game.GetVehicles(), false, coords, modelFilter)
end
---@param entities table The entities to search through
---@param isPlayerEntities boolean Whether the entities are players
---@param coords table | vector3 The coords to search from
---@param maxDistance number The max distance to search within
---@return table
local function EnumerateEntitiesWithinDistance(entities, isPlayerEntities, coords, maxDistance)
local nearbyEntities = {}
if coords then
coords = vector3(coords.x, coords.y, coords.z)
else
local playerPed = ESX.PlayerData.ped
coords = GetEntityCoords(playerPed)
end
for k, entity in pairs(entities) do
local distance = #(coords - GetEntityCoords(entity))
if distance <= maxDistance then
nearbyEntities[#nearbyEntities + 1] = isPlayerEntities and k or entity
end
end
return nearbyEntities
end
---@param coords table | vector3 The coords to search from
---@param maxDistance number The max distance to search within
---@return table
function ESX.Game.GetPlayersInArea(coords, maxDistance)
return EnumerateEntitiesWithinDistance(ESX.Game.GetPlayers(true, true), true, coords, maxDistance)
end
---@param coords table | vector3 The coords to search from
---@param maxDistance number The max distance to search within
---@return table
function ESX.Game.GetVehiclesInArea(coords, maxDistance)
return EnumerateEntitiesWithinDistance(ESX.Game.GetVehicles(), false, coords, maxDistance)
end
---@param coords table | vector3 The coords to search from
---@param maxDistance number The max distance to search within
---@return boolean
function ESX.Game.IsSpawnPointClear(coords, maxDistance)
return #ESX.Game.GetVehiclesInArea(coords, maxDistance) == 0
end
---@param shape integer The shape to get the test result from
---@return boolean, table, table, integer, integer
function ESX.Game.GetShapeTestResultSync(shape)
local handle, hit, coords, normal, material, entity
repeat
handle, hit, coords, normal, material, entity = GetShapeTestResultIncludingMaterial(shape)
Wait(0)
until handle ~= 1
return hit, coords, normal, material, entity
end
---@param depth number The depth to raycast
---@vararg any The arguments to pass to the shape test
---@return table, boolean, table, table, integer, integer
function ESX.Game.RaycastScreen(depth, ...)
local world, normal = GetWorldCoordFromScreenCoord(.5, .5)
local origin = world + normal
local target = world + normal * depth
return target, ESX.Game.GetShapeTestResultSync(StartShapeTestLosProbe(origin.x, origin.y, origin.z, target.x, target.y, target.z, ...))
end
---@param entities table The entities to search through
---@param isPlayerEntities boolean Whether the entities are players
---@param coords? table | vector3 The coords to search from
---@param modelFilter? table The model filter
---@return integer, integer
function ESX.Game.GetClosestEntity(entities, isPlayerEntities, coords, modelFilter)
local closestEntity, closestEntityDistance, filteredEntities = -1, -1, nil
if coords then
coords = vector3(coords.x, coords.y, coords.z)
else
local playerPed = ESX.PlayerData.ped
coords = GetEntityCoords(playerPed)
end
if modelFilter then
filteredEntities = {}
for currentEntityIndex = 1, #entities do
if modelFilter[GetEntityModel(entities[currentEntityIndex])] then
filteredEntities[#filteredEntities + 1] = entities[currentEntityIndex]
end
end
end
for k, entity in pairs(filteredEntities or entities) do
local distance = #(coords - GetEntityCoords(entity))
if closestEntityDistance == -1 or distance < closestEntityDistance then
closestEntity, closestEntityDistance = isPlayerEntities and k or entity, distance
end
end
return closestEntity, closestEntityDistance
end
---@return integer | nil, vector3 | nil
function ESX.Game.GetVehicleInDirection()
local _, hit, coords, _, _, entity = ESX.Game.RaycastScreen(5, 10, ESX.PlayerData.ped)
if hit and IsEntityAVehicle(entity) then
return entity, coords
end
end
---@param vehicle integer The vehicle to get the properties of
---@return table | nil
function ESX.Game.GetVehicleProperties(vehicle)
if not DoesEntityExist(vehicle) then
return
end
---@type number | number[], number | number[]
local colorPrimary, colorSecondary = GetVehicleColours(vehicle)
local pearlescentColor, wheelColor = GetVehicleExtraColours(vehicle)
local dashboardColor = GetVehicleDashboardColor(vehicle)
local interiorColor = GetVehicleInteriorColour(vehicle)
if GetIsVehiclePrimaryColourCustom(vehicle) then
colorPrimary = { GetVehicleCustomPrimaryColour(vehicle) }
end
if GetIsVehicleSecondaryColourCustom(vehicle) then
colorSecondary = { GetVehicleCustomSecondaryColour(vehicle) }
end
local hasCustomXenonColor, customXenonColorR, customXenonColorG, customXenonColorB = GetVehicleXenonLightsCustomColor(vehicle)
local customXenonColor = nil
if hasCustomXenonColor then
customXenonColor = { customXenonColorR, customXenonColorG, customXenonColorB }
end
local extras = {}
for extraId = 0, 20 do
if DoesExtraExist(vehicle, extraId) then
extras[tostring(extraId)] = IsVehicleExtraTurnedOn(vehicle, extraId)
end
end
local doorsBroken, windowsBroken, tyreBurst = {}, {}, {}
local numWheels = tostring(GetVehicleNumberOfWheels(vehicle))
local TyresIndex = { -- Wheel index list according to the number of vehicle wheels.
["2"] = { 0, 4 }, -- Bike and cycle.
["3"] = { 0, 1, 4, 5 }, -- Vehicle with 3 wheels (get for wheels because some 3 wheels vehicles have 2 wheels on front and one rear or the reverse).
["4"] = { 0, 1, 4, 5 }, -- Vehicle with 4 wheels.
["6"] = { 0, 1, 2, 3, 4, 5 }, -- Vehicle with 6 wheels.
}
if TyresIndex[numWheels] then
for _, idx in pairs(TyresIndex[numWheels]) do
tyreBurst[tostring(idx)] = IsVehicleTyreBurst(vehicle, idx, false)
end
end
for windowId = 0, 7 do -- 13
RollUpWindow(vehicle, windowId) --fix when you put the car away with the window down
windowsBroken[tostring(windowId)] = not IsVehicleWindowIntact(vehicle, windowId)
end
local numDoors = GetNumberOfVehicleDoors(vehicle)
if numDoors and numDoors > 0 then
for doorsId = 0, numDoors do
doorsBroken[tostring(doorsId)] = IsVehicleDoorDamaged(vehicle, doorsId)
end
end
return {
model = GetEntityModel(vehicle),
doorsBroken = doorsBroken,
windowsBroken = windowsBroken,
tyreBurst = tyreBurst,
tyresCanBurst = GetVehicleTyresCanBurst(vehicle),
plate = ESX.Math.Trim(GetVehicleNumberPlateText(vehicle)),
plateIndex = GetVehicleNumberPlateTextIndex(vehicle),
bodyHealth = ESX.Math.Round(GetVehicleBodyHealth(vehicle), 1),
engineHealth = ESX.Math.Round(GetVehicleEngineHealth(vehicle), 1),
tankHealth = ESX.Math.Round(GetVehiclePetrolTankHealth(vehicle), 1),
fuelLevel = ESX.Math.Round(GetVehicleFuelLevel(vehicle), 1),
dirtLevel = ESX.Math.Round(GetVehicleDirtLevel(vehicle), 1),
color1 = colorPrimary,
color2 = colorSecondary,
pearlescentColor = pearlescentColor,
wheelColor = wheelColor,
dashboardColor = dashboardColor,
interiorColor = interiorColor,
wheels = GetVehicleWheelType(vehicle),
windowTint = GetVehicleWindowTint(vehicle),
xenonColor = GetVehicleXenonLightsColor(vehicle),
customXenonColor = customXenonColor,
neonEnabled = { IsVehicleNeonLightEnabled(vehicle, 0), IsVehicleNeonLightEnabled(vehicle, 1), IsVehicleNeonLightEnabled(vehicle, 2), IsVehicleNeonLightEnabled(vehicle, 3) },
neonColor = table.pack(GetVehicleNeonLightsColour(vehicle)),
extras = extras,
tyreSmokeColor = table.pack(GetVehicleTyreSmokeColor(vehicle)),
modSpoilers = GetVehicleMod(vehicle, 0),
modFrontBumper = GetVehicleMod(vehicle, 1),
modRearBumper = GetVehicleMod(vehicle, 2),
modSideSkirt = GetVehicleMod(vehicle, 3),
modExhaust = GetVehicleMod(vehicle, 4),
modFrame = GetVehicleMod(vehicle, 5),
modGrille = GetVehicleMod(vehicle, 6),
modHood = GetVehicleMod(vehicle, 7),
modFender = GetVehicleMod(vehicle, 8),
modRightFender = GetVehicleMod(vehicle, 9),
modRoof = GetVehicleMod(vehicle, 10),
modRoofLivery = GetVehicleRoofLivery(vehicle),
modEngine = GetVehicleMod(vehicle, 11),
modBrakes = GetVehicleMod(vehicle, 12),
modTransmission = GetVehicleMod(vehicle, 13),
modHorns = GetVehicleMod(vehicle, 14),
modSuspension = GetVehicleMod(vehicle, 15),
modArmor = GetVehicleMod(vehicle, 16),
modTurbo = IsToggleModOn(vehicle, 18),
modSmokeEnabled = IsToggleModOn(vehicle, 20),
modXenon = IsToggleModOn(vehicle, 22),
modFrontWheels = GetVehicleMod(vehicle, 23),
modCustomFrontWheels = GetVehicleModVariation(vehicle, 23),
modBackWheels = GetVehicleMod(vehicle, 24),
modCustomBackWheels = GetVehicleModVariation(vehicle, 24),
modPlateHolder = GetVehicleMod(vehicle, 25),
modVanityPlate = GetVehicleMod(vehicle, 26),
modTrimA = GetVehicleMod(vehicle, 27),
modOrnaments = GetVehicleMod(vehicle, 28),
modDashboard = GetVehicleMod(vehicle, 29),
modDial = GetVehicleMod(vehicle, 30),
modDoorSpeaker = GetVehicleMod(vehicle, 31),
modSeats = GetVehicleMod(vehicle, 32),
modSteeringWheel = GetVehicleMod(vehicle, 33),
modShifterLeavers = GetVehicleMod(vehicle, 34),
modAPlate = GetVehicleMod(vehicle, 35),
modSpeakers = GetVehicleMod(vehicle, 36),
modTrunk = GetVehicleMod(vehicle, 37),
modHydrolic = GetVehicleMod(vehicle, 38),
modEngineBlock = GetVehicleMod(vehicle, 39),
modAirFilter = GetVehicleMod(vehicle, 40),
modStruts = GetVehicleMod(vehicle, 41),
modArchCover = GetVehicleMod(vehicle, 42),
modAerials = GetVehicleMod(vehicle, 43),
modTrimB = GetVehicleMod(vehicle, 44),
modTank = GetVehicleMod(vehicle, 45),
modWindows = GetVehicleMod(vehicle, 46),
modLivery = GetVehicleMod(vehicle, 48) == -1 and GetVehicleLivery(vehicle) or GetVehicleMod(vehicle, 48),
modLightbar = GetVehicleMod(vehicle, 49),
}
end
---@param vehicle integer The vehicle to set the properties of
---@param props table The properties to set
---@return nil
function ESX.Game.SetVehicleProperties(vehicle, props)
if not DoesEntityExist(vehicle) then
return
end
local colorPrimary, colorSecondary = GetVehicleColours(vehicle)
local pearlescentColor, wheelColor = GetVehicleExtraColours(vehicle)
SetVehicleModKit(vehicle, 0)
if props.tyresCanBurst ~= nil then
SetVehicleTyresCanBurst(vehicle, props.tyresCanBurst)
end
if props.plate ~= nil then
SetVehicleNumberPlateText(vehicle, props.plate)
end
if props.plateIndex ~= nil then
SetVehicleNumberPlateTextIndex(vehicle, props.plateIndex)
end
if props.bodyHealth ~= nil then
SetVehicleBodyHealth(vehicle, props.bodyHealth + 0.0)
end
if props.engineHealth ~= nil then
SetVehicleEngineHealth(vehicle, props.engineHealth + 0.0)
end
if props.tankHealth ~= nil then
SetVehiclePetrolTankHealth(vehicle, props.tankHealth + 0.0)
end
if props.fuelLevel ~= nil then
SetVehicleFuelLevel(vehicle, props.fuelLevel + 0.0)
end
if props.dirtLevel ~= nil then
SetVehicleDirtLevel(vehicle, props.dirtLevel + 0.0)
end
if props.color1 ~= nil then
if type(props.color1) == "table" then
SetVehicleCustomPrimaryColour(vehicle, props.color1[1], props.color1[2], props.color1[3])
else
SetVehicleColours(vehicle, props.color1, colorSecondary)
end
end
if props.color2 ~= nil then
if type(props.color2) == "table" then
SetVehicleCustomSecondaryColour(vehicle, props.color2[1], props.color2[2], props.color2[3])
else
SetVehicleColours(vehicle, props.color1 or colorPrimary, props.color2)
end
end
if props.pearlescentColor ~= nil then
SetVehicleExtraColours(vehicle, props.pearlescentColor, wheelColor)
end
if props.interiorColor ~= nil then
SetVehicleInteriorColor(vehicle, props.interiorColor)
end
if props.dashboardColor ~= nil then
SetVehicleDashboardColor(vehicle, props.dashboardColor)
end
if props.wheelColor ~= nil then
SetVehicleExtraColours(vehicle, props.pearlescentColor or pearlescentColor, props.wheelColor)
end
if props.wheels ~= nil then
SetVehicleWheelType(vehicle, props.wheels)
end
if props.windowTint ~= nil then
SetVehicleWindowTint(vehicle, props.windowTint)
end
if props.neonEnabled ~= nil then
SetVehicleNeonLightEnabled(vehicle, 0, props.neonEnabled[1])
SetVehicleNeonLightEnabled(vehicle, 1, props.neonEnabled[2])
SetVehicleNeonLightEnabled(vehicle, 2, props.neonEnabled[3])
SetVehicleNeonLightEnabled(vehicle, 3, props.neonEnabled[4])
end
if props.extras ~= nil then
for extraId, enabled in pairs(props.extras) do
extraId = tonumber(extraId)
if extraId then
SetVehicleExtra(vehicle, extraId, not enabled)
end
end
end
if props.neonColor ~= nil then
SetVehicleNeonLightsColour(vehicle, props.neonColor[1], props.neonColor[2], props.neonColor[3])
end
if props.xenonColor ~= nil then
SetVehicleXenonLightsColor(vehicle, props.xenonColor)
end
if props.customXenonColor ~= nil then
SetVehicleXenonLightsCustomColor(vehicle, props.customXenonColor[1], props.customXenonColor[2], props.customXenonColor[3])
end
if props.modSmokeEnabled ~= nil then
ToggleVehicleMod(vehicle, 20, true)
end
if props.tyreSmokeColor ~= nil then
SetVehicleTyreSmokeColor(vehicle, props.tyreSmokeColor[1], props.tyreSmokeColor[2], props.tyreSmokeColor[3])
end
if props.modSpoilers ~= nil then
SetVehicleMod(vehicle, 0, props.modSpoilers, false)
end
if props.modFrontBumper ~= nil then
SetVehicleMod(vehicle, 1, props.modFrontBumper, false)
end
if props.modRearBumper ~= nil then
SetVehicleMod(vehicle, 2, props.modRearBumper, false)
end
if props.modSideSkirt ~= nil then
SetVehicleMod(vehicle, 3, props.modSideSkirt, false)
end
if props.modExhaust ~= nil then
SetVehicleMod(vehicle, 4, props.modExhaust, false)
end
if props.modFrame ~= nil then
SetVehicleMod(vehicle, 5, props.modFrame, false)
end
if props.modGrille ~= nil then
SetVehicleMod(vehicle, 6, props.modGrille, false)
end
if props.modHood ~= nil then
SetVehicleMod(vehicle, 7, props.modHood, false)
end
if props.modFender ~= nil then
SetVehicleMod(vehicle, 8, props.modFender, false)
end
if props.modRightFender ~= nil then
SetVehicleMod(vehicle, 9, props.modRightFender, false)
end
if props.modRoof ~= nil then
SetVehicleMod(vehicle, 10, props.modRoof, false)
end
if props.modRoofLivery ~= nil then
SetVehicleRoofLivery(vehicle, props.modRoofLivery)
end
if props.modEngine ~= nil then
SetVehicleMod(vehicle, 11, props.modEngine, false)
end
if props.modBrakes ~= nil then
SetVehicleMod(vehicle, 12, props.modBrakes, false)
end
if props.modTransmission ~= nil then
SetVehicleMod(vehicle, 13, props.modTransmission, false)
end
if props.modHorns ~= nil then
SetVehicleMod(vehicle, 14, props.modHorns, false)
end
if props.modSuspension ~= nil then
SetVehicleMod(vehicle, 15, props.modSuspension, false)
end
if props.modArmor ~= nil then
SetVehicleMod(vehicle, 16, props.modArmor, false)
end
if props.modTurbo ~= nil then
ToggleVehicleMod(vehicle, 18, props.modTurbo)
end
if props.modXenon ~= nil then
ToggleVehicleMod(vehicle, 22, props.modXenon)
end
if props.modFrontWheels ~= nil then
SetVehicleMod(vehicle, 23, props.modFrontWheels, props.modCustomFrontWheels)
end
if props.modBackWheels ~= nil then
SetVehicleMod(vehicle, 24, props.modBackWheels, props.modCustomBackWheels)
end
if props.modPlateHolder ~= nil then
SetVehicleMod(vehicle, 25, props.modPlateHolder, false)
end
if props.modVanityPlate ~= nil then
SetVehicleMod(vehicle, 26, props.modVanityPlate, false)
end
if props.modTrimA ~= nil then
SetVehicleMod(vehicle, 27, props.modTrimA, false)
end
if props.modOrnaments ~= nil then
SetVehicleMod(vehicle, 28, props.modOrnaments, false)
end
if props.modDashboard ~= nil then
SetVehicleMod(vehicle, 29, props.modDashboard, false)
end
if props.modDial ~= nil then
SetVehicleMod(vehicle, 30, props.modDial, false)
end
if props.modDoorSpeaker ~= nil then
SetVehicleMod(vehicle, 31, props.modDoorSpeaker, false)
end
if props.modSeats ~= nil then
SetVehicleMod(vehicle, 32, props.modSeats, false)
end
if props.modSteeringWheel ~= nil then
SetVehicleMod(vehicle, 33, props.modSteeringWheel, false)
end
if props.modShifterLeavers ~= nil then
SetVehicleMod(vehicle, 34, props.modShifterLeavers, false)
end
if props.modAPlate ~= nil then
SetVehicleMod(vehicle, 35, props.modAPlate, false)
end
if props.modSpeakers ~= nil then
SetVehicleMod(vehicle, 36, props.modSpeakers, false)
end
if props.modTrunk ~= nil then
SetVehicleMod(vehicle, 37, props.modTrunk, false)
end
if props.modHydrolic ~= nil then
SetVehicleMod(vehicle, 38, props.modHydrolic, false)
end
if props.modEngineBlock ~= nil then
SetVehicleMod(vehicle, 39, props.modEngineBlock, false)
end
if props.modAirFilter ~= nil then
SetVehicleMod(vehicle, 40, props.modAirFilter, false)
end
if props.modStruts ~= nil then
SetVehicleMod(vehicle, 41, props.modStruts, false)
end
if props.modArchCover ~= nil then
SetVehicleMod(vehicle, 42, props.modArchCover, false)
end
if props.modAerials ~= nil then
SetVehicleMod(vehicle, 43, props.modAerials, false)
end
if props.modTrimB ~= nil then
SetVehicleMod(vehicle, 44, props.modTrimB, false)
end
if props.modTank ~= nil then
SetVehicleMod(vehicle, 45, props.modTank, false)
end
if props.modWindows ~= nil then
SetVehicleMod(vehicle, 46, props.modWindows, false)
end
if props.modLivery ~= nil then
SetVehicleMod(vehicle, 48, props.modLivery, false)
SetVehicleLivery(vehicle, props.modLivery)
end
if props.windowsBroken ~= nil then
for k, v in pairs(props.windowsBroken) do
if v then
k = tonumber(k)
if k then
RemoveVehicleWindow(vehicle, k)
end
end
end
end
if props.doorsBroken ~= nil then
for k, v in pairs(props.doorsBroken) do
if v then
k = tonumber(k)
if k then
SetVehicleDoorBroken(vehicle, k, true)
end
end
end
end
if props.tyreBurst ~= nil then
for k, v in pairs(props.tyreBurst) do
if v then
k = tonumber(k)
if k then
SetVehicleTyreBurst(vehicle, k, true, 1000.0)
end
end
end
end
end
---@param coords vector3 | table coords to get the closest pickup to
---@param text string The text to display
---@param size? number The size of the text
@@ -1,53 +0,0 @@
local Point = ESX.Class()
local nearby, loop = {}, nil
function Point:constructor(properties)
self.coords = properties.coords
self.hidden = properties.hidden
self.enter = properties.enter
self.leave = properties.leave
self.inside = properties.inside
self.handle = ESX.CreatePointInternal(properties.coords, properties.distance, properties.hidden, function()
nearby[self.handle] = self
if self.enter then
self:enter()
end
if not loop then
loop = true
CreateThread(function()
while loop do
local coords = GetEntityCoords(ESX.PlayerData.ped)
for _, point in pairs(nearby) do
if point.inside then
point:inside(#(coords - point.coords))
end
end
Wait(0)
end
end)
end
end, function()
nearby[self.handle] = nil
if self.leave then
self:leave()
end
if #nearby == 0 then
loop = false
end
end)
end
function Point:delete()
ESX.RemovePointInternal(self.handle)
end
function Point:toggle(hidden)
if hidden == nil then
hidden = not self.hidden
end
self.hidden = hidden
ESX.HidePointInternal(self.handle, hidden)
end
return Point
+29 -43
View File
@@ -1,10 +1,14 @@
-- ESX glue over the generic xLib.cache.
-- ped and weapon are tracked by the lib cache; this module mirrors them into
-- ESX.PlayerData, re-emits the legacy esx: events resources depend on, and keeps
-- the vehicle enter/exit state machine that produces the richer vehicle events.
Actions = {}
Actions._index = Actions
Actions.inVehicle = false
Actions.enteringVehicle = false
Actions.inPauseMenu = false
Actions.currentWeapon = false
function Actions:GetSeatPedIsIn()
for i = -1, 16 do
@@ -55,21 +59,6 @@ function Actions:TrackPedCoordsOnce()
end)
end
function Actions:TrackPed()
local playerPed = ESX.PlayerData.ped
local newPed = PlayerPedId()
if playerPed ~= newPed then
ESX.SetPlayerData("ped", newPed)
TriggerEvent("esx:playerPedChanged", newPed)
if Config.EnableDebug then
print("[DEBUG] Player ped changed:", newPed)
end
end
end
function Actions:TrackPauseMenu()
local isActive = IsPauseMenuActive()
@@ -189,46 +178,43 @@ function Actions:TrackSeat()
end
end
function Actions:TrackWeapon()
---@type number|false
local newWeapon = GetSelectedPedWeapon(ESX.PlayerData.ped)
newWeapon = newWeapon ~= `WEAPON_UNARMED` and newWeapon or false
if newWeapon ~= self.currentWeapon then
self.currentWeapon = newWeapon
ESX.SetPlayerData("weapon", self.currentWeapon)
TriggerEvent("esx:weaponChanged", self.currentWeapon)
if Config.EnableDebug then
print("[DEBUG] Weapon changed:", self.currentWeapon)
end
end
end
function Actions:SlowLoop()
CreateThread(function()
while ESX.PlayerLoaded do
self:TrackPauseMenu()
self:TrackVehicle()
self:TrackWeapon()
Wait(500)
end
end)
end
function Actions:PedLoop()
CreateThread(function()
while ESX.PlayerLoaded do
self:TrackPed()
Wait(0)
end
end)
end
function Actions:Init()
-- Re-seed the cached values on every (re)login. The change handlers below are
-- registered once at file load so a relogin never stacks duplicate handlers.
ESX.SetPlayerData("ped", xLib.cache.ped)
ESX.SetPlayerData("weapon", xLib.cache.weapon)
self:SlowLoop()
self:PedLoop()
self:TrackPedCoordsOnce()
end
-- Mirror the lib cache into ESX.PlayerData and re-emit the legacy esx: events.
AddEventHandler("xLib:cache:ped", function(ped)
ESX.SetPlayerData("ped", ped)
TriggerEvent("esx:playerPedChanged", ped)
if Config.EnableDebug then
print("[DEBUG] Player ped changed:", ped)
end
end)
AddEventHandler("xLib:cache:weapon", function(weapon)
ESX.SetPlayerData("weapon", weapon)
TriggerEvent("esx:weaponChanged", weapon)
if Config.EnableDebug then
print("[DEBUG] Weapon changed:", weapon)
end
end)
Actions:Init()
@@ -1,38 +0,0 @@
local interactions = {}
local pressedInteractions = {}
function ESX.RemoveInteraction(name)
if not interactions[name] then return end
interactions[name] = nil
end
ESX.RegisterInteraction = function(name, onPress, condition)
interactions[name] = {
condition = condition or function() return true end,
onPress = onPress,
creator = GetInvokingResource() or "es_extended"
}
end
function ESX.GetInteractKey()
local hash = joaat('esx_interact') | 0x80000000
return GetControlInstructionalButton(0, hash, true):sub(3)
end
ESX.RegisterInput("esx_interact", "Interact", "keyboard", "e", function()
for _, interaction in pairs(interactions) do
local success, result = pcall(interaction.condition)
if success and result then
pressedInteractions[#pressedInteractions+1] = interaction
interaction.onPress()
end
end
end)
AddEventHandler("onResourceStop", function(resource)
for name, interaction in pairs(interactions) do
if interaction.creator == resource then
interactions[name] = nil
end
end
end)
@@ -1,54 +0,0 @@
local points = {}
function ESX.CreatePointInternal(coords, distance, hidden, enter, leave)
local point = {
coords = coords,
distance = distance,
hidden = hidden,
enter = enter,
leave = leave,
resource = GetInvokingResource()
}
local handle = ESX.Table.SizeOf(points) + 1
points[handle] = point
return handle
end
function ESX.RemovePointInternal(handle)
points[handle] = nil
end
function ESX.HidePointInternal(handle, hidden)
if points[handle] then
points[handle].hidden = hidden
end
end
function StartPointsLoop()
CreateThread(function()
while true do
local coords = GetEntityCoords(ESX.PlayerData.ped)
for handle, point in pairs(points) do
if not point.hidden and #(coords - point.coords) <= point.distance then
if not point.nearby then
points[handle].nearby = true
points[handle].enter()
end
elseif point.nearby then
points[handle].nearby = false
points[handle].leave()
end
end
Wait(500)
end
end)
end
AddEventHandler('onResourceStop', function(resource)
for handle, point in pairs(points) do
if point.resource == resource then
points[handle] = nil
end
end
end)
+5 -4
View File
@@ -6,6 +6,7 @@ lua54 'yes'
version '1.13.5'
shared_scripts {
'@esx_lib/imports.lua',
'locale.lua',
'shared/config/main.lua',
@@ -15,6 +16,7 @@ shared_scripts {
'shared/main.lua',
'shared/functions.lua',
'shared/modules/*.lua',
'shared/compat.lua'
}
server_scripts {
@@ -39,24 +41,23 @@ server_scripts {
'server/modules/createJob.lua',
'server/migration/**/main.lua',
'server/migration/main.lua',
'server/compat.lua'
}
client_scripts {
'client/main.lua',
'client/functions.lua',
'client/compat.lua',
'client/modules/wrapper.lua',
'client/modules/callback.lua',
'client/modules/adjustments.lua',
'client/modules/points.lua',
'client/modules/events.lua',
'client/modules/actions.lua',
'client/modules/death.lua',
'client/modules/npwd.lua',
'client/modules/interactions.lua',
'client/modules/scaleform.lua',
'client/modules/streaming.lua',
}
ui_page {
+1 -1
View File
@@ -87,7 +87,7 @@ if not IsDuplicityVersion() then -- Only register this event for the client
end)
end
local external = { { "Class", "class.lua" }, { "Point", "point.lua" } }
local external = { { "Class", "class.lua" } }
for i = 1, #external do
local module = external[i]
local path = string.format("client/imports/%s", module[2])
+10
View File
@@ -0,0 +1,10 @@
--All server-side functions outsourced from the Core to the lib will be stored here for compatability, e.g:
ESX.OneSync.GetPlayersInArea = xLib.onesync.getPlayersInArea
ESX.OneSync.GetClosestPlayer = xLib.onesync.getClosestPlayer
ESX.OneSync.GetPedsInArea = xLib.onesync.getPedsInArea
ESX.OneSync.GetObjectsInArea = xLib.onesync.getObjectsInArea
ESX.OneSync.GetVehiclesInArea = xLib.onesync.getVehiclesInArea
ESX.OneSync.GetClosestPed = xLib.onesync.getClosestPed
ESX.OneSync.GetClosestObject = xLib.onesync.getClosestObject
ESX.OneSync.GetClosestVehicle = xLib.onesync.getClosestVehicle
@@ -1,84 +1,5 @@
ESX.OneSync = {}
---@param source number|vector3
---@param closest boolean
---@param distance? number
---@param ignore? table
---@param routingBucket? number
local function getNearbyPlayers(source, closest, distance, ignore, routingBucket)
local result = {}
local count = 0
local playerPed
local playerCoords
ignore = ignore or {}
if not distance then
distance = 100
end
if type(source) == "number" then
playerPed = GetPlayerPed(source)
if not source then
error("Received invalid first argument (source); should be playerId")
end
playerCoords = GetEntityCoords(playerPed)
if not playerCoords then
error("Received nil value (playerCoords); perhaps source is nil at first place?")
end
end
if type(source) == "vector3" then
playerCoords = source
if not playerCoords then
error("Received nil value (playerCoords); perhaps source is nil at first place?")
end
end
for _, xPlayer in pairs(ESX.Players) do
if not ignore[xPlayer.source] and (not routingBucket or GetPlayerRoutingBucket(xPlayer.source) == routingBucket) then
local entity = GetPlayerPed(xPlayer.source)
local coords = GetEntityCoords(entity)
if not closest then
local dist = #(playerCoords - coords)
if dist <= distance then
count = count + 1
result[count] = { id = xPlayer.source, ped = NetworkGetNetworkIdFromEntity(entity), coords = coords, dist = dist }
end
else
if xPlayer.source ~= source then
local dist = #(playerCoords - coords)
if dist <= (result.dist or distance) then
result = { id = xPlayer.source, ped = NetworkGetNetworkIdFromEntity(entity), coords = coords, dist = dist }
end
end
end
end
end
return result
end
---@param source vector3|number playerId or vector3 coordinates
---@param maxDistance number
---@param ignore? table playerIds to ignore, where the key is playerId and value is true
---@param routingBucket? number
function ESX.OneSync.GetPlayersInArea(source, maxDistance, ignore, routingBucket)
return getNearbyPlayers(source, false, maxDistance, ignore, routingBucket)
end
---@param source vector3|number playerId or vector3 coordinates
---@param maxDistance number
---@param ignore? table playerIds to ignore, where the key is playerId and value is true
---@param routingBucket? number
function ESX.OneSync.GetClosestPlayer(source, maxDistance, ignore, routingBucket)
return getNearbyPlayers(source, true, maxDistance, ignore, routingBucket)
end
---@param vehicleModel number|string
---@param coords vector3|table
---@param heading number
@@ -304,84 +225,3 @@ function ESX.OneSync.SpawnPedInVehicle(model, vehicle, seat, cb)
return Citizen.Await(promise)
end
end
local function getNearbyEntities(entities, coords, modelFilter, maxDistance, isPed)
local nearbyEntities = {}
coords = type(coords) == "number" and GetEntityCoords(GetPlayerPed(coords)) or vector3(coords.x, coords.y, coords.z)
for _, entity in pairs(entities) do
if not isPed or (isPed and not IsPedAPlayer(entity)) then
if not modelFilter or modelFilter[GetEntityModel(entity)] then
local entityCoords = GetEntityCoords(entity)
if not maxDistance or #(coords - entityCoords) <= maxDistance then
nearbyEntities[#nearbyEntities + 1] = NetworkGetNetworkIdFromEntity(entity)
end
end
end
end
return nearbyEntities
end
---@param coords vector3
---@param maxDistance number
---@param modelFilter table models to ignore, where the key is the model hash and the value is true
---@return table
function ESX.OneSync.GetPedsInArea(coords, maxDistance, modelFilter)
return getNearbyEntities(GetAllPeds(), coords, modelFilter, maxDistance, true)
end
---@param coords vector3
---@param maxDistance number
---@param modelFilter table models to ignore, where the key is the model hash and the value is true
---@return table
function ESX.OneSync.GetObjectsInArea(coords, maxDistance, modelFilter)
return getNearbyEntities(GetAllObjects(), coords, modelFilter, maxDistance)
end
---@param coords vector3
---@param maxDistance number
---@param modelFilter table | nil models to ignore, where the key is the model hash and the value is true
---@return table
function ESX.OneSync.GetVehiclesInArea(coords, maxDistance, modelFilter)
return getNearbyEntities(GetAllVehicles(), coords, modelFilter, maxDistance)
end
local function getClosestEntity(entities, coords, modelFilter, isPed)
local distance, closestEntity, closestCoords = 100, 0, vector3(0, 0, 0)
coords = type(coords) == "number" and GetEntityCoords(GetPlayerPed(coords)) or vector3(coords.x, coords.y, coords.z)
for _, entity in pairs(entities) do
if not isPed or (isPed and not IsPedAPlayer(entity)) then
if not modelFilter or modelFilter[GetEntityModel(entity)] then
local entityCoords = GetEntityCoords(entity)
local dist = #(coords - entityCoords)
if dist < distance then
closestEntity, distance, closestCoords = entity, dist, entityCoords
end
end
end
end
return NetworkGetNetworkIdFromEntity(closestEntity), distance, closestCoords
end
---@param coords vector3
---@param modelFilter table models to ignore, where the key is the model hash and the value is true
---@return number entityId, number distance, vector3 coords
function ESX.OneSync.GetClosestPed(coords, modelFilter)
return getClosestEntity(GetAllPeds(), coords, modelFilter, true)
end
---@param coords vector3
---@param modelFilter table models to ignore, where the key is the model hash and the value is true
---@return number entityId, number distance, vector3 coords
function ESX.OneSync.GetClosestObject(coords, modelFilter)
return getClosestEntity(GetAllObjects(), coords, modelFilter)
end
---@param coords vector3
---@param modelFilter table models to ignore, where the key is the model hash and the value is true
---@return number entityId, number distance, vector3 coords
function ESX.OneSync.GetClosestVehicle(coords, modelFilter)
return getClosestEntity(GetAllVehicles(), coords, modelFilter)
end
+23
View File
@@ -0,0 +1,23 @@
--All shared functions outsourced from the Core to the lib will be stored here for compatability, e.g:
ESX.Table = {}
ESX.SetTimeout = xLib.timeout.setTimeout
ESX.ClearTimeout = xLib.timeout.clearTimeout
ESX.Await = xLib.waitFor
ESX.Table.SizeOf = xLib.table.sizeOf
ESX.Table.Set = xLib.table.set
ESX.Table.IndexOf = xLib.table.indexOf
ESX.Table.LastIndexOf = xLib.table.lastIndexOf
ESX.Table.Find = xLib.table.find
ESX.Table.FindIndex = xLib.table.findIndex
ESX.Table.Filter = xLib.table.filter
ESX.Table.Map = xLib.table.map
ESX.Table.Reverse = xLib.table.reverse
ESX.Table.Clone = xLib.table.clone
ESX.Table.Concat = xLib.table.concat
ESX.Table.Join = xLib.table.join
ESX.Table.TableContains = xLib.table.contains
ESX.Table.Sort = xLib.table.sort
ESX.Table.ToArray = xLib.table.toArray
ESX.Table.Wipe = xLib.table.wipe
-39
View File
@@ -178,45 +178,6 @@ function ESX.IsFunctionReference(val)
return typeVal == "function" or (typeVal == "table" and type(getmetatable(val)?.__call) == "function")
end
---@param conditionFunc function A function that is repeatedly called until it returns a truthy value or the timeout is exceeded.
---@param errorMessage? string Optional. If set, an error will be thrown with this message if the condition is not met within the timeout. If not set, no error will be thrown.
---@param timeoutMs? number Optional. The maximum time to wait (in milliseconds) for the condition to be met. Defaults to 1000ms.
---@return boolean, any: Returns success status and the returned value of the condition function.
function ESX.Await(conditionFunc, errorMessage, timeoutMs)
timeoutMs = timeoutMs or 1000
if timeoutMs < 0 then
error("Timeout should be a positive number.")
end
if not ESX.IsFunctionReference(conditionFunc) then
error("Condition Function should be a function reference.")
end
-- since errorMessage is optional, we only validate it if the user provided it.
if errorMessage then
ESX.AssertType(errorMessage, "string", "errorMessage should be a string.")
end
local invokingResource = GetInvokingResource()
local startTimeMs = GetGameTimer()
while GetGameTimer() - startTimeMs < timeoutMs do
local result = conditionFunc()
if result then
return true, result
end
Wait(0)
end
if errorMessage then
error(("[%s] -> %s"):format(invokingResource, errorMessage))
end
return false
end
---@param str string
---@param allowDigits boolean? Allow numbers if necessary
---@return boolean
-241
View File
@@ -1,241 +0,0 @@
ESX.Table = {}
-- nil proof alternative to #table
---@param t table
---@return number
function ESX.Table.SizeOf(t)
local count = 0
for _, _ in pairs(t) do
count = count + 1
end
return count
end
---@param t table
---@return table
function ESX.Table.Set(t)
local set = {}
for _, v in ipairs(t) do
set[v] = true
end
return set
end
---@param t table
---@param value any
---@return number
function ESX.Table.IndexOf(t, value)
for i = 1, #t, 1 do
if t[i] == value then
return i
end
end
return -1
end
---@param t table
---@param value any
---@return number
function ESX.Table.LastIndexOf(t, value)
for i = #t, 1, -1 do
if t[i] == value then
return i
end
end
return -1
end
---@param t table
---@param cb function
---@return any
function ESX.Table.Find(t, cb)
for i = 1, #t, 1 do
if cb(t[i]) then
return t[i]
end
end
return nil
end
---@param t table
---@param cb function
---@return number
function ESX.Table.FindIndex(t, cb)
for i = 1, #t, 1 do
if cb(t[i]) then
return i
end
end
return -1
end
---@param t table
---@param cb function
---@return table
function ESX.Table.Filter(t, cb)
local newTable = {}
for i = 1, #t, 1 do
if cb(t[i]) then
newTable[#newTable + 1] = t[i]
end
end
return newTable
end
---@param t table
---@param cb function
---@return table
function ESX.Table.Map(t, cb)
local newTable = {}
for i = 1, #t, 1 do
newTable[i] = cb(t[i], i)
end
return newTable
end
---@param t table
---@return table
function ESX.Table.Reverse(t)
local newTable = {}
for i = #t, 1, -1 do
table.insert(newTable, t[i])
end
return newTable
end
---@param t table
---@return table
function ESX.Table.Clone(t)
if type(t) ~= "table" then
return t
end
local meta = getmetatable(t)
local target = {}
for k, v in pairs(t) do
if type(v) == "table" then
target[k] = ESX.Table.Clone(v)
else
target[k] = v
end
end
setmetatable(target, meta)
return target
end
---@param t1 table
---@param t2 table
---@return table
function ESX.Table.Concat(t1, t2)
local t3 = ESX.Table.Clone(t1)
for i = 1, #t2, 1 do
table.insert(t3, t2[i])
end
return t3
end
---@param t table
---@param sep string
---@return string
function ESX.Table.Join(t, sep)
local str = ""
for i = 1, #t, 1 do
if i > 1 then
str = str .. (sep or ",")
end
str = str .. t[i]
end
return str
end
-- Credits: https://github.com/JonasDev99/qb-garages/blob/b0335d67cb72a6b9ac60f62a87fb3946f5c2f33d/server/main.lua#L5
---@param tab table
---@param val any
---@return boolean
function ESX.Table.TableContains(tab, val)
if type(val) == "table" then
for _, value in pairs(tab) do
if ESX.Table.TableContains(val, value) then
return true
end
end
return false
else
for _, value in pairs(tab) do
if value == val then
return true
end
end
end
return false
end
-- Credit: https://stackoverflow.com/a/15706820
-- Description: sort function for pairs
---@param t table
---@param order function
---@return function
function ESX.Table.Sort(t, order)
-- collect the keys
local keys = {}
for k, _ in pairs(t) do
keys[#keys + 1] = k
end
-- if order function given, sort by it by passing the table and keys a, b,
-- otherwise just sort the keys
if order then
table.sort(keys, function(a, b)
return order(t, a, b)
end)
else
table.sort(keys)
end
-- return the iterator function
local i = 0
return function()
i = i + 1
if keys[i] then
return keys[i], t[keys[i]]
end
end
end
---@param t table
---@return Array
function ESX.Table.ToArray(t)
local array = {}
for _, v in pairs(t) do
array[#array + 1] = v
end
return array
end
---@param t table
---@return table
function ESX.Table.Wipe(t)
return table.wipe(t)
end
+31
View File
@@ -0,0 +1,31 @@
fx_version 'cerulean'
game 'gta5'
use_experimental_fxv2_oal 'yes'
lua54 'yes'
author 'ESX Team'
version '0.01'
description 'Official ESX library'
files {
'imports.lua',
'imports/**/client.lua',
'imports/**/shared.lua',
}
shared_scripts {
'resource/init.lua',
'resource/**/shared.lua',
}
client_scripts {
'resource/**/client.lua',
}
server_scripts {
'resource/**/server.lua',
}
+118
View File
@@ -0,0 +1,118 @@
--! DISCLAIMER
--[[
https://github.com/overextended/ox_lib
This file is licensed under LGPL-3.0 or higher <https://www.gnu.org/licenses/lgpl-3.0.en.html>
Copyright © 2025 Linden <https://github.com/thelindat>
]]
-- Const Variables
local CONTEXT<const> = IsDuplicityVersion() and 'server' or 'client'
local LIB_NAME <const> = 'esx_lib'
local IS_DEBUG <const> = GetConvar('xLib:debug', 'false') == 'true'
---@alias Module table | function
---@class xLib
---@field name string
---@field side 'server' | 'client'
-------------------------------------------------
--- Core functions for modules
-------------------------------------------------
--- Function that returns nothing
--- Used as a "shortcut"
function Noop() end
---Loads a module into memory
---@param self xLib
---@param name string -- module name
---@return Module | nil
local function loadModule(self, name)
local directory = ('imports/%s'):format(name)
local chunk = LoadResourceFile(LIB_NAME, ('%s/%s.lua'):format(directory, CONTEXT))
local shared_chunk = LoadResourceFile(LIB_NAME, ('%s/shared.lua'):format(directory))
if shared_chunk then
chunk = chunk and ('%s\n%s'):format(shared_chunk, chunk) or shared_chunk
end
if not chunk then
return
end
-- Second argument is a chunk name
local fn, err = load(chunk, ('@@%s/imports/%s/%s.lua'):format(LIB_NAME, name, CONTEXT))
if not fn or err then
if shared_chunk then
print(('[%s] Found an error while importing - %s! Try updating %s to the latest version!'):format(LIB_NAME,
name, LIB_NAME))
-- Tries to load only shared.lua
fn, err = load(shared_chunk, ('@@%s/imports/%s/%s.lua'):format(LIB_NAME, name, 'shared'))
end
if not fn or err then
return error(('Completly failed importing module - %s; error - %s'):format(name, err))
end
end
local result = fn()
self[name] = result or Noop
return self[name]
end
---Function that is responsible for lazy loading
---@param self xLib
---@param index string -- name of the function/key that is called
---@param ... unknown -- function params
---@return Module
local function call(self, index, ...)
local module = rawget(self, index) -- uses rawget not to trigger any metamethods
-- Module Loading
if not module then
self[index] = Noop -- to prevent module from loading again if doesn't exists.
module = loadModule(self, index)
if not module then
local function method(...)
return exports[LIB_NAME][index](nil, ...)
end
if not ... then
self[index] = method
end
return method
end
end
return module
end
-------------------------------------------------
--- Environment Setup
-------------------------------------------------
-- Creation of xLib object
---@diagnostic disable-next-line: lowercase-global
local xLib = setmetatable({
name = LIB_NAME,
side = CONTEXT,
debug = IS_DEBUG
}, {
-- Lazy Loading - loads module only when accessed
__index = call,
__call = call,
})
-- Allows to xLib to be accessible in resource that imports a lib
_ENV.xLib = xLib
_ENV.require = xLib.require
@@ -0,0 +1,92 @@
--[[
https://github.com/overextended/ox_lib
This file is licensed under LGPL-3.0 or higher <https://www.gnu.org/licenses/lgpl-3.0.en.html>
Copyright © 2025 Linden <https://github.com/thelindat>
]]
---@class KeybindProps
---@field name string
---@field description string
---@field defaultMapper? string (see: https://docs.fivem.net/docs/game-references/input-mapper-parameter-ids/)
---@field defaultKey? string
---@field disabled? boolean
---@field disable? fun(self: CKeybind, toggle: boolean)
---@field onPressed? fun(self: CKeybind)
---@field onReleased? fun(self: CKeybind)
---@field remove? fun(self: CKeybind)
---@field [string] any
---@class CKeybind : KeybindProps
---@field currentKey string
---@field disabled boolean
---@field isPressed boolean
---@field hash number
---@field getCurrentKey fun(): string
---@field isControlPressed fun(): boolean
local keybinds = {}
local IsPauseMenuActive = IsPauseMenuActive
local GetControlInstructionalButton = GetControlInstructionalButton
local keybind_mt = {
disabled = false,
isPressed = false,
defaultKey = '',
defaultMapper = 'keyboard',
}
function keybind_mt:__index(index)
return index == 'currentKey' and self:getCurrentKey() or keybind_mt[index]
end
function keybind_mt:getCurrentKey()
return GetControlInstructionalButton(0, self.hash, true):sub(3)
end
function keybind_mt:isControlPressed()
return self.isPressed
end
function keybind_mt:disable(toggle)
self.disabled = toggle
end
function keybind_mt:remove()
if not keybinds[self.name] then return end
keybinds[self.name] = nil
end
---@param data KeybindProps
---@return CKeybind
function xLib.addKeybind(data)
data.hash = joaat("+" .. data.name) | 0x80000000
keybinds[data.name] = setmetatable(data, keybind_mt)
RegisterCommand('+' .. data.name, function()
if not keybinds[data.name] then return end
if data.disabled or IsPauseMenuActive() then return end
data.isPressed = true
if data.onPressed then data:onPressed() end
end)
RegisterCommand('-' .. data.name, function()
if not keybinds[data.name] then return end
if data.disabled or IsPauseMenuActive() then return end
data.isPressed = false
if data.onReleased then data:onReleased() end
end)
RegisterKeyMapping('+' .. data.name, data.description, data.defaultMapper, data.defaultKey)
SetTimeout(500, function()
TriggerEvent('chat:removeSuggestion', ('/+%s'):format(data.name))
TriggerEvent('chat:removeSuggestion', ('/-%s'):format(data.name))
end)
return data
end
return xLib.addKeybind
+89
View File
@@ -0,0 +1,89 @@
--[[
Generic client-side player state cache, modelled on ox_lib cache.
Framework agnostic: natives only, no ESX coupling.
Loaded per-resource VM through the lazy loader. PlayerPedId() and friends
return the same value in every client VM, so the cached values are correct
in each resource that requires it.
Exposes the live values as xLib.cache and emits `xLib:cache:<key>` (value, previous)
whenever a tracked value changes. `coords` is read on demand from the live ped
and never stored.
]]
local playerId = PlayerId()
local cache = setmetatable({
playerId = playerId,
ped = PlayerPedId(),
vehicle = false,
seat = false,
weapon = false,
}, {
__index = function(self, key)
if key == "coords" then
return GetEntityCoords(self.ped)
elseif key == "serverId" then
-- resolved lazily: GetPlayerServerId can return -1 before the network
-- session is ready, so only cache it once it is valid.
local id = GetPlayerServerId(playerId)
if id and id ~= -1 then
rawset(self, "serverId", id)
end
return id
end
end,
})
local function set(key, value)
if cache[key] == value then
return
end
local previous = cache[key]
rawset(cache, key, value)
TriggerEvent(("xLib:cache:%s"):format(key), value, previous)
end
local function getSeat(ped, vehicle)
for seat = -1, 16 do
if GetPedInVehicleSeat(vehicle, seat) == ped then
return seat
end
end
return false
end
CreateThread(function()
while true do
local ped = PlayerPedId()
if ped ~= cache.ped then
set("ped", ped)
end
---@type integer|false
local vehicle = GetVehiclePedIsIn(ped, false)
vehicle = vehicle ~= 0 and vehicle or false
if vehicle ~= cache.vehicle then
set("vehicle", vehicle)
set("seat", vehicle and getSeat(ped, vehicle) or false)
elseif vehicle then
local seat = getSeat(ped, vehicle)
if seat ~= cache.seat then
set("seat", seat)
end
end
---@type integer|false
local weapon = GetSelectedPedWeapon(ped)
weapon = weapon ~= `WEAPON_UNARMED` and weapon or false
if weapon ~= cache.weapon then
set("weapon", weapon)
end
Wait(100)
end
end)
return cache
+148
View File
@@ -0,0 +1,148 @@
--[[
https://github.com/overextended/ox_lib
This file is licensed under LGPL-3.0 or higher <https://www.gnu.org/licenses/lgpl-3.0.en.html>
Copyright © 2025 Linden <https://github.com/thelindat>
]]
local pendingCallbacks = {}
local timers = {}
local cbEvent = '__xLib_cb_%s'
local callbackTimeout = GetConvarInt('xLib:callbackTimeout', 300000)
local resource_name = GetCurrentResourceName() --TODO: Add cache
RegisterNetEvent(cbEvent:format(resource_name), function(key, ...)
if source == '' then return end
local cb = pendingCallbacks[key]
if not cb then return end
pendingCallbacks[key] = nil
cb(...)
end)
---@param event string
---@param delay? number | false prevent the event from being called for the given time
local function eventTimer(event, delay)
if xLib.verify(delay, 'number') then
if delay > 0 then
local time = GetGameTimer()
if (timers[event] or 0) > time then
return false
end
timers[event] = time + delay
end
end
return true
end
---@param _ any
---@param event string
---@param delay number | false | nil
---@param cb function | false
---@param ... any
---@return ...
local function triggerServerCallback(_, event, delay, cb, ...)
if not eventTimer(event, delay) then return end
local key
repeat
key = ('%s:%s'):format(event, math.random(0, 100000))
until not pendingCallbacks[key]
TriggerServerEvent('xLib:validateCallback', event, resource_name, key)
TriggerServerEvent(cbEvent:format(event), resource_name, key, ...)
---@type promise | false
local promise = not cb and promise.new()
pendingCallbacks[key] = function(response, ...)
if response == 'cb_invalid' then
response = ("callback '%s' does not exist"):format(event)
return promise and promise:reject(response) or error(response)
end
response = { response, ... }
if promise then
return promise:resolve(response)
end
if cb then
cb(table.unpack(response))
end
end
if promise then
SetTimeout(callbackTimeout, function() promise:reject(("callback event '%s' timed out"):format(key)) end)
return table.unpack(Citizen.Await(promise))
end
end
---@overload fun(event: string, delay: number | false, cb: function, ...)
xLib.callback = setmetatable({}, {
__call = function(_, event, delay, cb, ...)
if not cb then
warn(("callback event '%s' does not have a function to callback to and will instead await\nuse xLib.callback.await or a regular event to remove this warning")
:format(event))
else
local cbType = type(cb)
if cbType == 'table' and getmetatable(cb)?.__call then
cbType = 'function'
end
xLib.verify(cbType, 'function', true)
end
return triggerServerCallback(_, event, delay, cb, ...)
end
})
---@param event string
---@param delay? number | false prevent the event from being called for the given time.
---Sends an event to the server and halts the current thread until a response is returned.
---@diagnostic disable-next-line: duplicate-set-field
function xLib.callback.await(event, delay, ...)
return triggerServerCallback(nil, event, delay, false, ...)
end
local function callbackResponse(success, result, ...)
if not success then
if result then
return print(('^1SCRIPT ERROR: %s^0\n%s'):format(result,
Citizen.InvokeNative(`FORMAT_STACK_TRACE` & 0xFFFFFFFF, nil, 0, Citizen.ResultAsString()) or ''))
end
return false
end
return result, ...
end
local pcall = pcall
---@param name string
---@param cb function
---Registers an event handler and callback function to respond to server requests.
---@diagnostic disable-next-line: duplicate-set-field
function xLib.callback.register(name, cb)
local event = cbEvent:format(name)
xLib.setValidCallback(name, true)
RegisterNetEvent(event, function(resource, key, ...)
TriggerServerEvent(cbEvent:format(resource), key, callbackResponse(pcall(cb, ...)))
end)
end
return xLib.callback
+127
View File
@@ -0,0 +1,127 @@
--[[
https://github.com/overextended/ox_lib
This file is licensed under LGPL-3.0 or higher <https://www.gnu.org/licenses/lgpl-3.0.en.html>
Copyright © 2025 Linden <https://github.com/thelindat>
]]
local pendingCallbacks = {}
local cbEvent = '__xLib_cb_%s'
local callbackTimeout = GetConvarInt('xLib:callbackTimeout', 300000)
local resource_name = GetCurrentResourceName() --TODO: Add cache
RegisterNetEvent(cbEvent:format(resource_name), function(key, ...)
local cb = pendingCallbacks[key]
if not cb then return end
pendingCallbacks[key] = nil
cb(...)
end)
---@param _ any
---@param event string
---@param playerId number
---@param cb function|false
---@param ... any
---@return ...
local function triggerClientCallback(_, event, playerId, cb, ...)
xLib.verify(playerId, 'playerId', true)
local key
repeat
key = ('%s:%s:%s'):format(event, math.random(0, 100000), playerId)
until not pendingCallbacks[key]
TriggerClientEvent('xLib:validateCallback', playerId, event, resource_name, key)
TriggerClientEvent(cbEvent:format(event), playerId, resource_name, key, ...)
---@type promise | false
local promise = not cb and promise.new()
pendingCallbacks[key] = function(response, ...)
if response == 'cb_invalid' then
response = ("callback '%s' does not exist"):format(event)
return promise and promise:reject(response) or error(response)
end
response = { response, ... }
if promise then
return promise:resolve(response)
end
if cb then
cb(table.unpack(response))
end
end
if promise then
SetTimeout(callbackTimeout, function() promise:reject(("callback event '%s' timed out"):format(key)) end)
return table.unpack(Citizen.Await(promise))
end
end
---@overload fun(event: string, playerId: number, cb: function, ...)
xLib.callback = setmetatable({}, {
__call = function(_, event, playerId, cb, ...)
if not cb then
warn(("callback event '%s' does not have a function to callback to and will instead await\nuse xLib.callback.await or a regular event to remove this warning")
:format(event))
else
local cbType = type(cb)
if cbType == 'table' and getmetatable(cb)?.__call then
cbType = 'function'
end
xLib.verify(cbType, 'function', true)
end
return triggerClientCallback(_, event, playerId, cb, ...)
end
})
---@param event string
---@param playerId number
--- Sends an event to a client and halts the current thread until a response is returned.
---@diagnostic disable-next-line: duplicate-set-field
function xLib.callback.await(event, playerId, ...)
return triggerClientCallback(nil, event, playerId, false, ...)
end
local function callbackResponse(success, result, ...)
if not success then
if result then
return print(('^1SCRIPT ERROR: %s^0\n%s'):format(result,
Citizen.InvokeNative(`FORMAT_STACK_TRACE` & 0xFFFFFFFF, nil, 0, Citizen.ResultAsString()) or ''))
end
return false
end
return result, ...
end
local pcall = pcall
---@param name string
---@param cb function
---Registers an event handler and callback function to respond to client requests.
---@diagnostic disable-next-line: duplicate-set-field
function xLib.callback.register(name, cb)
event = cbEvent:format(name)
xLib.setValidCallback(name, true)
RegisterNetEvent(event, function(resource, key, ...)
TriggerClientEvent(cbEvent:format(resource), source, key, callbackResponse(pcall(cb, source, ...)))
end)
end
return xLib.callback
+35
View File
@@ -0,0 +1,35 @@
---@class xClass
---@field new function
---@field constructor function?
---Create or register a class
---@param copy? table
---@return table
function xLib.class(copy)
xLib.verify(copy, { 'table', 'nil' }, true)
local class = copy and xLib.table.deepcopy(copy) or {}
class.__index = class
class._isClass = true
setmetatable(class, {
__newindex = function (_, k,v)
rawset(class, k, v)
end
})
function class:new(...)
local obj = setmetatable({}, class)
if xLib.verify(obj.constructor, 'function') then
obj:constructor(...)
end
return obj
end
return class
end
return xLib.class
+10
View File
@@ -0,0 +1,10 @@
xLib.colors = {}
xLib.colors.brand = GetConvar("esx:brand-color", "#FB9B04")
xLib.colors.darkest = GetConvar("esx:darkest-color", "#161616")
xLib.colors.dark = GetConvar("esx:dark-color", "#252525")
xLib.colors.mid = GetConvar("esx:mid-color", "#383838")
xLib.colors.light = GetConvar("esx:light-color", "#969696")
xLib.colors.lightest = GetConvar("esx:lightest-color", "#F2F2F2")
return xLib.colors
+120
View File
@@ -0,0 +1,120 @@
--[[
https://github.com/overextended/ox_lib
This file is licensed under LGPL-3.0 or higher <https://www.gnu.org/licenses/lgpl-3.0.en.html>
Copyright © 2025 Linden <https://github.com/thelindat>
]]
---@class DuiProperties
---@field url string
---@field width number
---@field height number
---@field debug? boolean
---@class Dui
---@field private_id string
---@field private_debug boolean
---@field url string
---@field duiObject number
---@field duiHandle string
---@field runtimeTxd number
---@field txdObject number
---@field dictName string
---@field txtName string
xLib.dui = xLib.class()
local resource<const> = GetCurrentResourceName()
---@type table<string, Dui>
local Duis = {}
local currentId = 0
---@param data DuiProperties
function xLib.dui:constructor(data)
local time = GetGameTimer()
local id = ("%s_%s_%s"):format(resource, time, currentId)
currentId = currentId + 1
local dictName = ("%s_dui_dict_%s"):format(resource, id)
local txtName = ("%s_lib_dui_txt_%s"):format(resource, id)
local duiObject = CreateDui(data.url, data.width, data.height)
local duiHandle = GetDuiHandle(duiObject)
local runtimeTxd = CreateRuntimeTxd(dictName)
local txdObject = CreateRuntimeTextureFromDuiHandle(runtimeTxd, txtName, duiHandle)
self.private_id = id
self.private_debug = data.debug or false
self.url = data.url
self.duiObject = duiObject
self.duiHandle = duiHandle
self.runtimeTxd = runtimeTxd
self.txdObject = txdObject
self.dictName = dictName
self.txtName = txtName
Duis[id] = self
if self.private_debug then
print(("Dui %s created"):format(id))
end
end
function xLib.dui:remove()
SetDuiUrl(self.duiObject, "about:blank")
DestroyDui(self.duiObject)
Duis[self.private_id] = nil
if self.private_debug then
print(("Dui %s removed"):format(self.private_id))
end
end
---@param url string
function xLib.dui:setUrl(url)
self.url = url
SetDuiUrl(self.duiObject, url)
if self.private_debug then
print(("Dui %s url set to %s"):format(self.private_id, url))
end
end
---@param message table
function xLib.dui:sendMessage(message)
SendDuiMessage(self.duiObject, json.encode(message))
if self.private_debug then
print(("Dui %s message sent with data :"):format(self.private_id), json.encode(message, { indent = true }))
end
end
---@param x number
---@param y number
function xLib.dui:sendMouseMove(x, y)
SendDuiMouseMove(self.duiObject, x, y)
end
---@param button "left" | "middle" | "right"
function xLib.dui:sendMouseDown(button)
SendDuiMouseDown(self.duiObject, button)
end
---@param button "left" | "middle" | "right"
function xLib.dui:sendMouseUp(button)
SendDuiMouseUp(self.duiObject, button)
end
---@param deltaX number
---@param deltaY number
function xLib.dui:sendMouseWheel(deltaX, deltaY)
SendDuiMouseWheel(self.duiObject, deltaY, deltaX)
end
AddEventHandler("onResourceStop", function(resourceName)
if resource ~= resourceName then return end
for id in next, Duis do
Duis[id]:remove()
end
end)
return xLib.dui
+85
View File
@@ -0,0 +1,85 @@
xLib.entity = {}
---@param entities table The entities to search through
---@param isPlayerEntities boolean Whether the entities are players
---@param coords? table | vector3 The coords to search from
---@param modelFilter? table The model filter
---@return integer, integer
function xLib.entity.closest(entities, isPlayerEntities, coords, modelFilter)
local closestEntity, closestEntityDistance, filteredEntities = -1, -1, nil
if coords then
coords = vector3(coords.x, coords.y, coords.z)
else
local playerPed = PlayerPedId()
coords = GetEntityCoords(playerPed)
end
if modelFilter then
filteredEntities = {}
for currentEntityIndex = 1, #entities do
if modelFilter[GetEntityModel(entities[currentEntityIndex])] then
filteredEntities[#filteredEntities + 1] = entities[currentEntityIndex]
end
end
end
for k, entity in pairs(filteredEntities or entities) do
local distance = #(coords - GetEntityCoords(entity))
if closestEntityDistance == -1 or distance < closestEntityDistance then
closestEntity, closestEntityDistance = isPlayerEntities and k or entity, distance
end
end
return closestEntity, closestEntityDistance
end
---@param entities table The entities to search through
---@param isPlayerEntities boolean Whether the entities are players
---@param coords? table | vector3 The coords to search from
---@param maxDistance number The maximum distance
---@return table
function xLib.entity.EnumerateWithinDistance(entities, isPlayerEntities, coords, maxDistance)
local nearbyEntities = {}
if coords then
coords = vector3(coords.x, coords.y, coords.z)
else
local playerPed = PlayerPedId()
coords = GetEntityCoords(playerPed)
end
for k, entity in pairs(entities) do
local distance = #(coords - GetEntityCoords(entity))
if distance <= maxDistance then
nearbyEntities[#nearbyEntities + 1] = isPlayerEntities and k or entity
end
end
return nearbyEntities
end
---@param entity integer The entity to get the coords of
---@param coords table | vector3 | vector4 The coords to teleport the entity to
---@param cb? function The callback function
function xLib.entity.Teleport(entity, coords, cb)
if DoesEntityExist(entity) then
RequestCollisionAtCoord(coords.x, coords.y, coords.z)
while not HasCollisionLoadedAroundEntity(entity) do
Wait(0)
end
SetEntityCoords(entity, coords.x, coords.y, coords.z, false, false, false, false)
SetEntityHeading(entity, coords.w or coords.heading or 0.0)
end
if cb then
cb()
end
end
return xLib.entity
+695
View File
@@ -0,0 +1,695 @@
---@class gamelib
xLib.game = {}
local function isCallable(val)
local valType = type(val)
local mt = getmetatable(val)
return valType == "function" or (valType == "table" and mt ~= nil and type(mt.__call) == "function")
end
---@param ped integer The ped to get the mugshot of
---@param transparent? boolean Whether the mugshot should be transparent
function xLib.game.getPedMugshot(ped, transparent)
if not DoesEntityExist(ped) then
return
end
local mugshot = transparent and RegisterPedheadshotTransparent(ped) or RegisterPedheadshot(ped)
while not IsPedheadshotReady(mugshot) do
Wait(0)
end
return mugshot, GetPedheadshotTxdString(mugshot)
end
---@param object integer | string The object to spawn
---@param coords table | vector3 The coords to spawn the object at
---@param cb? function The callback function
---@param networked? boolean Whether the object should be networked
---@return integer | nil
function xLib.game.spawnObject(object, coords, cb, networked)
local model = type(object) == "number" and object or joaat(object)
xLib.streaming.requestModel(model)
local obj = CreateObject(model, coords.x, coords.y, coords.z, networked == nil or networked, false, true)
return cb and cb(obj) or obj
end
---@param object integer | string The object to spawn
---@param coords table | vector3 The coords to spawn the object at
---@param cb? function The callback function
---@return nil
function xLib.game.spawnLocalObject(object, coords, cb)
xLib.game.spawnObject(object, coords, cb, false)
end
---@param vehicle integer The vehicle to delete
---@return nil
function xLib.game.deleteVehicle(vehicle)
SetEntityAsMissionEntity(vehicle, true, true)
DeleteVehicle(vehicle)
end
---@param object integer The object to delete
---@return nil
function xLib.game.deleteObject(object)
SetEntityAsMissionEntity(object, false, true)
DeleteObject(object)
end
---@param vehicleModel integer | string The vehicle to spawn
---@param coords table | vector3 The coords to spawn the vehicle at
---@param heading number The heading of the vehicle
---@param cb? fun(vehicle: number) The callback function
---@param networked? boolean Whether the vehicle should be networked
---@return number? vehicle
function xLib.game.spawnVehicle(vehicleModel, coords, heading, cb, networked)
if cb and not isCallable(cb) then
error("Invalid callback function")
end
local model = type(vehicleModel) == "number" and vehicleModel or joaat(vehicleModel)
local vector = type(coords) == "vector3" and coords or vec(coords.x, coords.y, coords.z)
local isNetworked = networked == nil or networked
local playerCoords = GetEntityCoords(PlayerPedId())
if not vector or not playerCoords then
return
end
local dist = #(playerCoords - vector)
if dist > 424 then -- Onesync infinity Range (https://docs.fivem.net/docs/scripting-reference/onesync/)
local executingResource = GetInvokingResource() or "Unknown"
return error(("Resource ^5%s^1 Tried to spawn vehicle on the client but the position is too far away (Out of onesync range)."):format(executingResource))
end
local promise = not cb and promise.new()
CreateThread(function()
local modelHash = xLib.streaming.requestModel(model)
if not modelHash then
if promise then
promise:reject(("Tried to spawn invalid vehicle - ^5%s^7!"):format(model))
return
end
error(("Tried to spawn invalid vehicle - ^5%s^7!"):format(model))
end
local vehicle = CreateVehicle(model, vector.x, vector.y, vector.z, heading, isNetworked, true)
if networked then
local id = NetworkGetNetworkIdFromEntity(vehicle)
SetNetworkIdCanMigrate(id, true)
SetEntityAsMissionEntity(vehicle, true, true)
end
SetVehicleHasBeenOwnedByPlayer(vehicle, true)
SetVehicleNeedsToBeHotwired(vehicle, false)
SetModelAsNoLongerNeeded(model)
SetVehRadioStation(vehicle, "OFF")
RequestCollisionAtCoord(vector.x, vector.y, vector.z)
while not HasCollisionLoadedAroundEntity(vehicle) do
Wait(0)
end
if promise then
promise:resolve(vehicle)
elseif cb then
cb(vehicle)
end
end)
if promise then
return Citizen.Await(promise)
end
end
---@param vehicle integer The vehicle to spawn
---@param coords table | vector3 The coords to spawn the vehicle at
---@param heading number The heading of the vehicle
---@param cb? function The callback function
---@return nil
function xLib.game.spawnLocalVehicle(vehicle, coords, heading, cb)
xLib.game.spawnVehicle(vehicle, coords, heading, cb, false)
end
---@param vehicle integer The vehicle to check
---@return boolean
function xLib.game.isVehicleEmpty(vehicle)
return GetVehicleNumberOfPassengers(vehicle) == 0 and IsVehicleSeatFree(vehicle, -1)
end
---@return table
function xLib.game.getObjects() -- Leave the function for compatibility
return GetGamePool("CObject")
end
---@param onlyOtherPeds? boolean Whether to exlude the player ped
---@return table
function xLib.game.getPeds(onlyOtherPeds)
local pool = GetGamePool("CPed")
if onlyOtherPeds then
local myPed = PlayerPedId()
for i = 1, #pool do
if pool[i] == myPed then
table.remove(pool, i)
break
end
end
end
return pool
end
---@return table
function xLib.game.getVehicles() -- Leave the function for compatibility
return GetGamePool("CVehicle")
end
---@param onlyOtherPlayers? boolean Whether to exclude the player
---@param returnKeyValue? boolean Whether to return the key value pair
---@param returnPeds? boolean Whether to return the peds
---@return table
function xLib.game.getPlayers(onlyOtherPlayers, returnKeyValue, returnPeds)
local players = {}
local active = GetActivePlayers()
for i = 1, #active do
local currentPlayer = active[i]
local ped = GetPlayerPed(currentPlayer)
if DoesEntityExist(ped) and ((onlyOtherPlayers and currentPlayer ~= PlayerId()) or not onlyOtherPlayers) then
if returnKeyValue then
players[currentPlayer] = ped
else
players[#players + 1] = returnPeds and ped or currentPlayer
end
end
end
return players
end
---@param coords? table | vector3 The coords to get the closest object to
---@param modelFilter? table The model filter
---@return integer, integer
function xLib.game.getClosestObject(coords, modelFilter)
return xLib.entity.closest(xLib.game.getObjects(), false, coords, modelFilter)
end
---@param coords? table | vector3 The coords to get the closest ped to
---@param modelFilter? table The model filter
---@return integer, integer
function xLib.game.getClosestPed(coords, modelFilter)
return xLib.entity.closest(xLib.game.getPeds(true), false, coords, modelFilter)
end
---@param coords? table | vector3 The coords to get the closest player to
---@return integer, integer
function xLib.game.getClosestPlayer(coords)
return xLib.entity.closest(xLib.game.getPlayers(true, true), true, coords, nil)
end
---@param coords? table | vector3 The coords to get the closest vehicle to
---@param modelFilter? table The model filter
---@return integer, integer
function xLib.game.getClosestVehicle(coords, modelFilter)
return xLib.entity.closest(xLib.game.getVehicles(), false, coords, modelFilter)
end
---@param coords table | vector3 The coords to search from
---@param maxDistance number The max distance to search within
---@return table
function xLib.game.getPlayersInArea(coords, maxDistance)
return xLib.entity.EnumerateWithinDistance(xLib.game.getPlayers(true, true), true, coords, maxDistance)
end
---@param coords table | vector3 The coords to search from
---@param maxDistance number The max distance to search within
---@return table
function xLib.game.getVehiclesInArea(coords, maxDistance)
return xLib.entity.EnumerateWithinDistance(xLib.game.getVehicles(), false, coords, maxDistance)
end
---@param coords table | vector3 The coords to search from
---@param maxDistance number The max distance to search within
---@return boolean
function xLib.game.isSpawnPointClear(coords, maxDistance)
return #xLib.game.getVehiclesInArea(coords, maxDistance) == 0
end
---@return integer | nil, vector3 | nil
function xLib.game.getVehicleInDirection()
local _, hit, coords, _, _, entity = xLib.Raycast.FromScreen(5, 10, PlayerPedId())
if hit and IsEntityAVehicle(entity) then
return entity, coords
end
end
local function round(value, numDecimalPlaces)
if numDecimalPlaces then
local power = 10 ^ numDecimalPlaces
return math.floor((value * power) + 0.5) / power
else
return math.floor(value + 0.5)
end
end
local function trim(value)
value = tostring(value)
return (string.gsub(value, "^%s*(.-)%s*$", "%1"))
end
---@param vehicle integer The vehicle to get the properties of
---@return table | nil
function xLib.game.getVehicleProperties(vehicle)
if not DoesEntityExist(vehicle) then
return
end
---@type number | number[], number | number[]
local colorPrimary, colorSecondary = GetVehicleColours(vehicle)
local pearlescentColor, wheelColor = GetVehicleExtraColours(vehicle)
local dashboardColor = GetVehicleDashboardColor(vehicle)
local interiorColor = GetVehicleInteriorColour(vehicle)
if GetIsVehiclePrimaryColourCustom(vehicle) then
colorPrimary = { GetVehicleCustomPrimaryColour(vehicle) }
end
if GetIsVehicleSecondaryColourCustom(vehicle) then
colorSecondary = { GetVehicleCustomSecondaryColour(vehicle) }
end
local hasCustomXenonColor, customXenonColorR, customXenonColorG, customXenonColorB = GetVehicleXenonLightsCustomColor(vehicle)
local customXenonColor = nil
if hasCustomXenonColor then
customXenonColor = { customXenonColorR, customXenonColorG, customXenonColorB }
end
local extras = {}
for extraId = 0, 20 do
if DoesExtraExist(vehicle, extraId) then
extras[tostring(extraId)] = IsVehicleExtraTurnedOn(vehicle, extraId)
end
end
local doorsBroken, windowsBroken, tyreBurst = {}, {}, {}
local numWheels = tostring(GetVehicleNumberOfWheels(vehicle))
local TyresIndex = { -- Wheel index list according to the number of vehicle wheels.
["2"] = { 0, 4 }, -- Bike and cycle.
["3"] = { 0, 1, 4, 5 }, -- Vehicle with 3 wheels (get for wheels because some 3 wheels vehicles have 2 wheels on front and one rear or the reverse).
["4"] = { 0, 1, 4, 5 }, -- Vehicle with 4 wheels.
["6"] = { 0, 1, 2, 3, 4, 5 }, -- Vehicle with 6 wheels.
}
if TyresIndex[numWheels] then
for _, idx in pairs(TyresIndex[numWheels]) do
tyreBurst[tostring(idx)] = IsVehicleTyreBurst(vehicle, idx, false)
end
end
for windowId = 0, 7 do -- 13
RollUpWindow(vehicle, windowId) --fix when you put the car away with the window down
windowsBroken[tostring(windowId)] = not IsVehicleWindowIntact(vehicle, windowId)
end
local numDoors = GetNumberOfVehicleDoors(vehicle)
if numDoors and numDoors > 0 then
for doorsId = 0, numDoors do
doorsBroken[tostring(doorsId)] = IsVehicleDoorDamaged(vehicle, doorsId)
end
end
return {
model = GetEntityModel(vehicle),
doorsBroken = doorsBroken,
windowsBroken = windowsBroken,
tyreBurst = tyreBurst,
tyresCanBurst = GetVehicleTyresCanBurst(vehicle),
plate = trim(GetVehicleNumberPlateText(vehicle)),
plateIndex = GetVehicleNumberPlateTextIndex(vehicle),
bodyHealth = round(GetVehicleBodyHealth(vehicle), 1),
engineHealth = round(GetVehicleEngineHealth(vehicle), 1),
tankHealth = round(GetVehiclePetrolTankHealth(vehicle), 1),
fuelLevel = round(GetVehicleFuelLevel(vehicle), 1),
dirtLevel = round(GetVehicleDirtLevel(vehicle), 1),
color1 = colorPrimary,
color2 = colorSecondary,
pearlescentColor = pearlescentColor,
wheelColor = wheelColor,
dashboardColor = dashboardColor,
interiorColor = interiorColor,
wheels = GetVehicleWheelType(vehicle),
windowTint = GetVehicleWindowTint(vehicle),
xenonColor = GetVehicleXenonLightsColor(vehicle),
customXenonColor = customXenonColor,
neonEnabled = { IsVehicleNeonLightEnabled(vehicle, 0), IsVehicleNeonLightEnabled(vehicle, 1), IsVehicleNeonLightEnabled(vehicle, 2), IsVehicleNeonLightEnabled(vehicle, 3) },
neonColor = table.pack(GetVehicleNeonLightsColour(vehicle)),
extras = extras,
tyreSmokeColor = table.pack(GetVehicleTyreSmokeColor(vehicle)),
modSpoilers = GetVehicleMod(vehicle, 0),
modFrontBumper = GetVehicleMod(vehicle, 1),
modRearBumper = GetVehicleMod(vehicle, 2),
modSideSkirt = GetVehicleMod(vehicle, 3),
modExhaust = GetVehicleMod(vehicle, 4),
modFrame = GetVehicleMod(vehicle, 5),
modGrille = GetVehicleMod(vehicle, 6),
modHood = GetVehicleMod(vehicle, 7),
modFender = GetVehicleMod(vehicle, 8),
modRightFender = GetVehicleMod(vehicle, 9),
modRoof = GetVehicleMod(vehicle, 10),
modRoofLivery = GetVehicleRoofLivery(vehicle),
modEngine = GetVehicleMod(vehicle, 11),
modBrakes = GetVehicleMod(vehicle, 12),
modTransmission = GetVehicleMod(vehicle, 13),
modHorns = GetVehicleMod(vehicle, 14),
modSuspension = GetVehicleMod(vehicle, 15),
modArmor = GetVehicleMod(vehicle, 16),
modTurbo = IsToggleModOn(vehicle, 18),
modSmokeEnabled = IsToggleModOn(vehicle, 20),
modXenon = IsToggleModOn(vehicle, 22),
modFrontWheels = GetVehicleMod(vehicle, 23),
modCustomFrontWheels = GetVehicleModVariation(vehicle, 23),
modBackWheels = GetVehicleMod(vehicle, 24),
modCustomBackWheels = GetVehicleModVariation(vehicle, 24),
modPlateHolder = GetVehicleMod(vehicle, 25),
modVanityPlate = GetVehicleMod(vehicle, 26),
modTrimA = GetVehicleMod(vehicle, 27),
modOrnaments = GetVehicleMod(vehicle, 28),
modDashboard = GetVehicleMod(vehicle, 29),
modDial = GetVehicleMod(vehicle, 30),
modDoorSpeaker = GetVehicleMod(vehicle, 31),
modSeats = GetVehicleMod(vehicle, 32),
modSteeringWheel = GetVehicleMod(vehicle, 33),
modShifterLeavers = GetVehicleMod(vehicle, 34),
modAPlate = GetVehicleMod(vehicle, 35),
modSpeakers = GetVehicleMod(vehicle, 36),
modTrunk = GetVehicleMod(vehicle, 37),
modHydrolic = GetVehicleMod(vehicle, 38),
modEngineBlock = GetVehicleMod(vehicle, 39),
modAirFilter = GetVehicleMod(vehicle, 40),
modStruts = GetVehicleMod(vehicle, 41),
modArchCover = GetVehicleMod(vehicle, 42),
modAerials = GetVehicleMod(vehicle, 43),
modTrimB = GetVehicleMod(vehicle, 44),
modTank = GetVehicleMod(vehicle, 45),
modWindows = GetVehicleMod(vehicle, 46),
modLivery = GetVehicleMod(vehicle, 48) == -1 and GetVehicleLivery(vehicle) or GetVehicleMod(vehicle, 48),
modLightbar = GetVehicleMod(vehicle, 49),
}
end
---@param vehicle integer The vehicle to set the properties of
---@param props table The properties to set
---@return nil
function xLib.game.setVehicleProperties(vehicle, props)
if not DoesEntityExist(vehicle) then
return
end
local colorPrimary, colorSecondary = GetVehicleColours(vehicle)
local pearlescentColor, wheelColor = GetVehicleExtraColours(vehicle)
SetVehicleModKit(vehicle, 0)
if props.tyresCanBurst ~= nil then
SetVehicleTyresCanBurst(vehicle, props.tyresCanBurst)
end
if props.plate ~= nil then
SetVehicleNumberPlateText(vehicle, props.plate)
end
if props.plateIndex ~= nil then
SetVehicleNumberPlateTextIndex(vehicle, props.plateIndex)
end
if props.bodyHealth ~= nil then
SetVehicleBodyHealth(vehicle, props.bodyHealth + 0.0)
end
if props.engineHealth ~= nil then
SetVehicleEngineHealth(vehicle, props.engineHealth + 0.0)
end
if props.tankHealth ~= nil then
SetVehiclePetrolTankHealth(vehicle, props.tankHealth + 0.0)
end
if props.fuelLevel ~= nil then
SetVehicleFuelLevel(vehicle, props.fuelLevel + 0.0)
end
if props.dirtLevel ~= nil then
SetVehicleDirtLevel(vehicle, props.dirtLevel + 0.0)
end
if props.color1 ~= nil then
if type(props.color1) == "table" then
SetVehicleCustomPrimaryColour(vehicle, props.color1[1], props.color1[2], props.color1[3])
else
SetVehicleColours(vehicle, props.color1, colorSecondary)
end
end
if props.color2 ~= nil then
if type(props.color2) == "table" then
SetVehicleCustomSecondaryColour(vehicle, props.color2[1], props.color2[2], props.color2[3])
else
SetVehicleColours(vehicle, props.color1 or colorPrimary, props.color2)
end
end
if props.pearlescentColor ~= nil then
SetVehicleExtraColours(vehicle, props.pearlescentColor, wheelColor)
end
if props.interiorColor ~= nil then
SetVehicleInteriorColor(vehicle, props.interiorColor)
end
if props.dashboardColor ~= nil then
SetVehicleDashboardColor(vehicle, props.dashboardColor)
end
if props.wheelColor ~= nil then
SetVehicleExtraColours(vehicle, props.pearlescentColor or pearlescentColor, props.wheelColor)
end
if props.wheels ~= nil then
SetVehicleWheelType(vehicle, props.wheels)
end
if props.windowTint ~= nil then
SetVehicleWindowTint(vehicle, props.windowTint)
end
if props.neonEnabled ~= nil then
SetVehicleNeonLightEnabled(vehicle, 0, props.neonEnabled[1])
SetVehicleNeonLightEnabled(vehicle, 1, props.neonEnabled[2])
SetVehicleNeonLightEnabled(vehicle, 2, props.neonEnabled[3])
SetVehicleNeonLightEnabled(vehicle, 3, props.neonEnabled[4])
end
if props.extras ~= nil then
for extraId, enabled in pairs(props.extras) do
extraId = tonumber(extraId)
if extraId then
SetVehicleExtra(vehicle, extraId, not enabled)
end
end
end
if props.neonColor ~= nil then
SetVehicleNeonLightsColour(vehicle, props.neonColor[1], props.neonColor[2], props.neonColor[3])
end
if props.xenonColor ~= nil then
SetVehicleXenonLightsColor(vehicle, props.xenonColor)
end
if props.customXenonColor ~= nil then
SetVehicleXenonLightsCustomColor(vehicle, props.customXenonColor[1], props.customXenonColor[2], props.customXenonColor[3])
end
if props.modSmokeEnabled ~= nil then
ToggleVehicleMod(vehicle, 20, true)
end
if props.tyreSmokeColor ~= nil then
SetVehicleTyreSmokeColor(vehicle, props.tyreSmokeColor[1], props.tyreSmokeColor[2], props.tyreSmokeColor[3])
end
if props.modSpoilers ~= nil then
SetVehicleMod(vehicle, 0, props.modSpoilers, false)
end
if props.modFrontBumper ~= nil then
SetVehicleMod(vehicle, 1, props.modFrontBumper, false)
end
if props.modRearBumper ~= nil then
SetVehicleMod(vehicle, 2, props.modRearBumper, false)
end
if props.modSideSkirt ~= nil then
SetVehicleMod(vehicle, 3, props.modSideSkirt, false)
end
if props.modExhaust ~= nil then
SetVehicleMod(vehicle, 4, props.modExhaust, false)
end
if props.modFrame ~= nil then
SetVehicleMod(vehicle, 5, props.modFrame, false)
end
if props.modGrille ~= nil then
SetVehicleMod(vehicle, 6, props.modGrille, false)
end
if props.modHood ~= nil then
SetVehicleMod(vehicle, 7, props.modHood, false)
end
if props.modFender ~= nil then
SetVehicleMod(vehicle, 8, props.modFender, false)
end
if props.modRightFender ~= nil then
SetVehicleMod(vehicle, 9, props.modRightFender, false)
end
if props.modRoof ~= nil then
SetVehicleMod(vehicle, 10, props.modRoof, false)
end
if props.modRoofLivery ~= nil then
SetVehicleRoofLivery(vehicle, props.modRoofLivery)
end
if props.modEngine ~= nil then
SetVehicleMod(vehicle, 11, props.modEngine, false)
end
if props.modBrakes ~= nil then
SetVehicleMod(vehicle, 12, props.modBrakes, false)
end
if props.modTransmission ~= nil then
SetVehicleMod(vehicle, 13, props.modTransmission, false)
end
if props.modHorns ~= nil then
SetVehicleMod(vehicle, 14, props.modHorns, false)
end
if props.modSuspension ~= nil then
SetVehicleMod(vehicle, 15, props.modSuspension, false)
end
if props.modArmor ~= nil then
SetVehicleMod(vehicle, 16, props.modArmor, false)
end
if props.modTurbo ~= nil then
ToggleVehicleMod(vehicle, 18, props.modTurbo)
end
if props.modXenon ~= nil then
ToggleVehicleMod(vehicle, 22, props.modXenon)
end
if props.modFrontWheels ~= nil then
SetVehicleMod(vehicle, 23, props.modFrontWheels, props.modCustomFrontWheels)
end
if props.modBackWheels ~= nil then
SetVehicleMod(vehicle, 24, props.modBackWheels, props.modCustomBackWheels)
end
if props.modPlateHolder ~= nil then
SetVehicleMod(vehicle, 25, props.modPlateHolder, false)
end
if props.modVanityPlate ~= nil then
SetVehicleMod(vehicle, 26, props.modVanityPlate, false)
end
if props.modTrimA ~= nil then
SetVehicleMod(vehicle, 27, props.modTrimA, false)
end
if props.modOrnaments ~= nil then
SetVehicleMod(vehicle, 28, props.modOrnaments, false)
end
if props.modDashboard ~= nil then
SetVehicleMod(vehicle, 29, props.modDashboard, false)
end
if props.modDial ~= nil then
SetVehicleMod(vehicle, 30, props.modDial, false)
end
if props.modDoorSpeaker ~= nil then
SetVehicleMod(vehicle, 31, props.modDoorSpeaker, false)
end
if props.modSeats ~= nil then
SetVehicleMod(vehicle, 32, props.modSeats, false)
end
if props.modSteeringWheel ~= nil then
SetVehicleMod(vehicle, 33, props.modSteeringWheel, false)
end
if props.modShifterLeavers ~= nil then
SetVehicleMod(vehicle, 34, props.modShifterLeavers, false)
end
if props.modAPlate ~= nil then
SetVehicleMod(vehicle, 35, props.modAPlate, false)
end
if props.modSpeakers ~= nil then
SetVehicleMod(vehicle, 36, props.modSpeakers, false)
end
if props.modTrunk ~= nil then
SetVehicleMod(vehicle, 37, props.modTrunk, false)
end
if props.modHydrolic ~= nil then
SetVehicleMod(vehicle, 38, props.modHydrolic, false)
end
if props.modEngineBlock ~= nil then
SetVehicleMod(vehicle, 39, props.modEngineBlock, false)
end
if props.modAirFilter ~= nil then
SetVehicleMod(vehicle, 40, props.modAirFilter, false)
end
if props.modStruts ~= nil then
SetVehicleMod(vehicle, 41, props.modStruts, false)
end
if props.modArchCover ~= nil then
SetVehicleMod(vehicle, 42, props.modArchCover, false)
end
if props.modAerials ~= nil then
SetVehicleMod(vehicle, 43, props.modAerials, false)
end
if props.modTrimB ~= nil then
SetVehicleMod(vehicle, 44, props.modTrimB, false)
end
if props.modTank ~= nil then
SetVehicleMod(vehicle, 45, props.modTank, false)
end
if props.modWindows ~= nil then
SetVehicleMod(vehicle, 46, props.modWindows, false)
end
if props.modLivery ~= nil then
SetVehicleMod(vehicle, 48, props.modLivery, false)
SetVehicleLivery(vehicle, props.modLivery)
end
if props.windowsBroken ~= nil then
for k, v in pairs(props.windowsBroken) do
if v then
k = tonumber(k)
if k then
RemoveVehicleWindow(vehicle, k)
end
end
end
end
if props.doorsBroken ~= nil then
for k, v in pairs(props.doorsBroken) do
if v then
k = tonumber(k)
if k then
SetVehicleDoorBroken(vehicle, k, true)
end
end
end
end
if props.tyreBurst ~= nil then
for k, v in pairs(props.tyreBurst) do
if v then
k = tonumber(k)
if k then
SetVehicleTyreBurst(vehicle, k, true, 1000.0)
end
end
end
end
end
return xLib.game
@@ -0,0 +1,54 @@
---@class interactionslib
xLib.interactions = {}
local interactions = {}
local pressedInteractions = {}
---@param name string
function xLib.interactions.remove(name)
if not interactions[name] then return end
interactions[name] = nil
end
---@param name string
---@param onPress function
---@param condition? function
function xLib.interactions.register(name, onPress, condition)
interactions[name] = {
condition = condition or function() return true end,
onPress = onPress,
creator = GetInvokingResource() or "es_extended"
}
end
---@return string
function xLib.interactions.getInteractKey()
local hash = joaat("esx_interact") | 0x80000000
return GetControlInstructionalButton(0, hash, true):sub(3)
end
xLib.addKeybind({
name = "esx_interact",
description = "Interact",
defaultMapper = "keyboard",
defaultKey = "e",
onPressed = function()
for _, interaction in pairs(interactions) do
local success, result = pcall(interaction.condition)
if success and result then
pressedInteractions[#pressedInteractions + 1] = interaction
interaction.onPress()
end
end
end
})
AddEventHandler("onResourceStop", function(resource)
for name, interaction in pairs(interactions) do
if interaction.creator == resource then
interactions[name] = nil
end
end
end)
return xLib.interactions
+164
View File
@@ -0,0 +1,164 @@
---@class onesynclib
xLib.onesync = {}
---@param source number|vector3
---@param closest boolean
---@param distance? number
---@param ignore? table
---@param routingBucket? number
local function getNearbyPlayers(source, closest, distance, ignore, routingBucket)
local result = {}
local count = 0
local playerPed
local playerCoords
ignore = ignore or {}
if not distance then
distance = 100
end
if type(source) == "number" then
playerPed = GetPlayerPed(source)
if not source then
error("Received invalid first argument (source); should be playerId")
end
playerCoords = GetEntityCoords(playerPed)
if not playerCoords then
error("Received nil value (playerCoords); perhaps source is nil at first place?")
end
end
if type(source) == "vector3" then
playerCoords = source
if not playerCoords then
error("Received nil value (playerCoords); perhaps source is nil at first place?")
end
end
for _, xPlayer in pairs(ESX.Players) do
if not ignore[xPlayer.source] and (not routingBucket or GetPlayerRoutingBucket(xPlayer.source) == routingBucket) then
local entity = GetPlayerPed(xPlayer.source)
local coords = GetEntityCoords(entity)
if not closest then
local dist = #(playerCoords - coords)
if dist <= distance then
count = count + 1
result[count] = { id = xPlayer.source, ped = NetworkGetNetworkIdFromEntity(entity), coords = coords, dist = dist }
end
else
if xPlayer.source ~= source then
local dist = #(playerCoords - coords)
if dist <= (result.dist or distance) then
result = { id = xPlayer.source, ped = NetworkGetNetworkIdFromEntity(entity), coords = coords, dist = dist }
end
end
end
end
end
return result
end
---@param source vector3|number playerId or vector3 coordinates
---@param maxDistance number
---@param ignore? table playerIds to ignore, where the key is playerId and value is true
---@param routingBucket? number
function xLib.onesync.getPlayersInArea(source, maxDistance, ignore, routingBucket)
return getNearbyPlayers(source, false, maxDistance, ignore, routingBucket)
end
---@param source vector3|number playerId or vector3 coordinates
---@param maxDistance number
---@param ignore? table playerIds to ignore, where the key is playerId and value is true
---@param routingBucket? number
function xLib.onesync.getClosestPlayer(source, maxDistance, ignore, routingBucket)
return getNearbyPlayers(source, true, maxDistance, ignore, routingBucket)
end
local function getNearbyEntities(entities, coords, modelFilter, maxDistance, isPed)
local nearbyEntities = {}
coords = type(coords) == "number" and GetEntityCoords(GetPlayerPed(coords)) or vector3(coords.x, coords.y, coords.z)
for _, entity in pairs(entities) do
if not isPed or (isPed and not IsPedAPlayer(entity)) then
if not modelFilter or modelFilter[GetEntityModel(entity)] then
local entityCoords = GetEntityCoords(entity)
if not maxDistance or #(coords - entityCoords) <= maxDistance then
nearbyEntities[#nearbyEntities + 1] = NetworkGetNetworkIdFromEntity(entity)
end
end
end
end
return nearbyEntities
end
---@param coords vector3
---@param maxDistance number
---@param modelFilter table models to ignore, where the key is the model hash and the value is true
---@return table
function xLib.onesync.getPedsInArea(coords, maxDistance, modelFilter)
return getNearbyEntities(GetAllPeds(), coords, modelFilter, maxDistance, true)
end
---@param coords vector3
---@param maxDistance number
---@param modelFilter table models to ignore, where the key is the model hash and the value is true
---@return table
function xLib.onesync.getObjectsInArea(coords, maxDistance, modelFilter)
return getNearbyEntities(GetAllObjects(), coords, modelFilter, maxDistance)
end
---@param coords vector3
---@param maxDistance number
---@param modelFilter table | nil models to ignore, where the key is the model hash and the value is true
---@return table
function xLib.onesync.getVehiclesInArea(coords, maxDistance, modelFilter)
return getNearbyEntities(GetAllVehicles(), coords, modelFilter, maxDistance)
end
local function getClosestEntity(entities, coords, modelFilter, isPed)
local distance, closestEntity, closestCoords = 100, 0, vector3(0, 0, 0)
coords = type(coords) == "number" and GetEntityCoords(GetPlayerPed(coords)) or vector3(coords.x, coords.y, coords.z)
for _, entity in pairs(entities) do
if not isPed or (isPed and not IsPedAPlayer(entity)) then
if not modelFilter or modelFilter[GetEntityModel(entity)] then
local entityCoords = GetEntityCoords(entity)
local dist = #(coords - entityCoords)
if dist < distance then
closestEntity, distance, closestCoords = entity, dist, entityCoords
end
end
end
end
return NetworkGetNetworkIdFromEntity(closestEntity), distance, closestCoords
end
---@param coords vector3
---@param modelFilter table models to ignore, where the key is the model hash and the value is true
---@return number entityId, number distance, vector3 coords
function xLib.onesync.getClosestPed(coords, modelFilter)
return getClosestEntity(GetAllPeds(), coords, modelFilter, true)
end
---@param coords vector3
---@param modelFilter table models to ignore, where the key is the model hash and the value is true
---@return number entityId, number distance, vector3 coords
function xLib.onesync.getClosestObject(coords, modelFilter)
return getClosestEntity(GetAllObjects(), coords, modelFilter)
end
---@param coords vector3
---@param modelFilter table models to ignore, where the key is the model hash and the value is true
---@return number entityId, number distance, vector3 coords
function xLib.onesync.getClosestVehicle(coords, modelFilter)
return getClosestEntity(GetAllVehicles(), coords, modelFilter)
end
return xLib.onesync
@@ -0,0 +1,66 @@
local overloads <const> = {}
---@param name string
---@param ... unknown
local function getOverloadFunction(name, ...)
local params = {...}
local params_count = #params
local is_valid, overload, correct_type
for i=1, #overloads[name] do
overload = overloads[name][i]
if not overload then
goto continue
end
is_valid = #overload.valid_types == params_count
if is_valid then
for j = 1, #overload.valid_types do
correct_type = overload.valid_types[j]
if not xLib.verify(params[j], correct_type) then
is_valid = false
break
end
end
if is_valid then
return overload.cb(...)
end
end
::continue::
end
error('[xLib] Couldn\'t find a correct method to overload')
end
---Overloads a function
---@param name string
---@param valid_types CustomType | CustomType[]
---@param cb function
---@param obj? table
function xLib.overload(name, valid_types, cb, obj)
xLib.verify(name, {'string'}, true)
if not overloads[name] then
overloads[name] = {}
end
overloads[name][#overloads[name] + 1] = {
valid_types = valid_types,
cb = cb
}
local env = obj and obj or _ENV
if not env[name] then
env[name] = function(...)
return getOverloadFunction(name, ...)
end
end
end
return xLib.overload
+52
View File
@@ -0,0 +1,52 @@
---@class xPoint
---@field coords vector3
---@field hidden? boolean
---@field enter? function
---@field leave? function
---@field inside? function
---@field handle integer
xLib.point = xLib.class()
---@param properties table coords, distance, hidden, enter, leave, inside
function xLib.point:constructor(properties)
self.coords = properties.coords
self.hidden = properties.hidden
self.enter = properties.enter
self.leave = properties.leave
self.inside = properties.inside
self.handle = xLib.points.create(
properties.coords,
properties.distance,
properties.hidden,
function()
if self.enter then
self:enter()
end
end,
function()
if self.leave then
self:leave()
end
end,
properties.inside and function(dist)
self:inside(dist)
end or nil
)
end
function xLib.point:delete()
xLib.points.remove(self.handle)
end
---@param hidden? boolean
function xLib.point:toggle(hidden)
if hidden == nil then
hidden = not self.hidden
end
self.hidden = hidden
xLib.points.hide(self.handle, hidden)
end
return xLib.point
+100
View File
@@ -0,0 +1,100 @@
---@class pointslib
xLib.points = {}
local points = {}
local insidePoints = {}
local handleCount = 0
---@param coords vector3
---@param distance number
---@param hidden? boolean
---@param enter function
---@param leave function
---@param inside? function
---@return integer handle
function xLib.points.create(coords, distance, hidden, enter, leave, inside)
handleCount = handleCount + 1
local handle = handleCount
points[handle] = {
coords = coords,
distance = distance,
hidden = hidden,
enter = enter,
leave = leave,
inside = inside,
resource = GetInvokingResource()
}
return handle
end
---@param handle integer
function xLib.points.remove(handle)
points[handle] = nil
insidePoints[handle] = nil
end
---@param handle integer
---@param hidden boolean
function xLib.points.hide(handle, hidden)
if points[handle] then
points[handle].hidden = hidden
end
end
function xLib.points.startLoop()
CreateThread(function()
local lastScan = 0
while true do
local coords = GetEntityCoords(PlayerPedId())
for _, point in pairs(insidePoints) do
point.inside(#(coords - point.coords))
end
local now = GetGameTimer()
if now - lastScan >= 500 then
lastScan = now
for handle, point in pairs(points) do
if not point.hidden and #(coords - point.coords) <= point.distance then
if not point.nearby then
point.nearby = true
if point.enter then
point.enter()
end
if point.inside then
insidePoints[handle] = point
end
end
elseif point.nearby then
point.nearby = false
if point.leave then
point.leave()
end
insidePoints[handle] = nil
end
end
end
Wait(next(insidePoints) and 0 or 500)
end
end)
end
AddEventHandler("onResourceStop", function(resource)
for handle, point in pairs(points) do
if point.resource == resource then
points[handle] = nil
insidePoints[handle] = nil
end
end
end)
return xLib.points
+79
View File
@@ -0,0 +1,79 @@
-- Per-VM client listener for xLib pub/sub. Routes incoming topic data to the
-- handlers registered with xLib.pubsub.on. on() only listens; the player must be
-- subscribed server-side to actually receive anything.
local PUBSUB_EVENT <const> = '__xLib_pubsub' -- must match resource/pubsub/server.lua
-- topic -> array of handlers
local handlers = {}
local pubsub = {}
---Register a handler for a topic. Multiple handlers per topic are allowed.
---@param topic string
---@param handler fun(data: any, topic: string)
function pubsub.on(topic, handler)
if type(topic) ~= 'string' or type(handler) ~= 'function' then
return
end
local list = handlers[topic]
if not list then
list = {}
handlers[topic] = list
end
list[#list + 1] = handler
end
---Remove handlers for a topic. Without a handler argument, removes all of them.
---@param topic string
---@param handler? function
function pubsub.off(topic, handler)
if not handler then
handlers[topic] = nil
return
end
local list = handlers[topic]
if not list then
return
end
for i = #list, 1, -1 do
if list[i] == handler then
table.remove(list, i)
end
end
if list[1] == nil then
handlers[topic] = nil
end
end
RegisterNetEvent(PUBSUB_EVENT, function(topic, data)
local list = handlers[topic]
if not list then
return
end
-- fast path: a topic almost always has a single handler, so skip the copy
local n = #list
if n == 1 then
list[1](data, topic)
return
end
-- snapshot so a handler that calls off() mid-dispatch cannot shift the list
-- and skip a sibling that has not run yet
local snapshot = {}
for i = 1, n do
snapshot[i] = list[i]
end
for i = 1, n do
snapshot[i](data, topic)
end
end)
xLib.pubsub = pubsub
return pubsub
+47
View File
@@ -0,0 +1,47 @@
-- Per-VM wrapper exposing xLib.pubsub.* on the server; forwards to the shared
-- registry (resource/pubsub/server.lua) through its exports.
local pubsub = {}
---@param src number serverId of the player to add
---@param topic string
---@return boolean added
function pubsub.subscribe(src, topic)
return xLib.pubsub_subscribe(src, topic)
end
---@param src number
---@param topic string
---@return boolean removed
function pubsub.unsubscribe(src, topic)
return xLib.pubsub_unsubscribe(src, topic)
end
---@param topic string
---@param data any server-trusted data only
---@return integer count number of players notified
function pubsub.publish(topic, data)
return xLib.pubsub_publish(topic, data)
end
---@param topic string
---@return number[] serverIds
function pubsub.subscribers(topic)
return xLib.pubsub_subscribers(topic)
end
---@param src number
---@param topic string
---@return boolean
function pubsub.isSubscribed(src, topic)
return xLib.pubsub_isSubscribed(src, topic)
end
---@param topic string
---@return integer count
function pubsub.clear(topic)
return xLib.pubsub_clear(topic)
end
xLib.pubsub = pubsub
return pubsub
+77
View File
@@ -0,0 +1,77 @@
xLib.Raycast = {}
xLib.Raycast.__index = xLib.Raycast
local unpack = table.unpack
local GetShapeTestResultIncludingMaterial = GetShapeTestResultIncludingMaterial
local StartShapeTestLosProbe = StartShapeTestLosProbe
local GetWorldCoordFromScreenCoord = GetWorldCoordFromScreenCoord
---@param shape integer The shape test handle to wait for
---@return boolean, table, table, integer, integer
function xLib.Raycast.GetShapeTestResult(shape)
local handle, hit, coords, normal, material, entity
repeat
handle, hit, coords, normal, material, entity = GetShapeTestResultIncludingMaterial(shape)
Wait(0)
until handle ~= 1
return hit, coords, normal, material, entity
end
---@param depth number The raycast distance
---@vararg any Additional arguments to pass to shape test
---@return table, boolean, table, table, integer, integer
function xLib.Raycast.FromScreen(depth, ...)
local worldCoords, normalVector = GetWorldCoordFromScreenCoord(0.5, 0.5)
local origin = worldCoords + normalVector
local target = worldCoords + normalVector * depth
return target, xLib.Raycast.GetShapeTestResult(StartShapeTestLosProbe(origin.x, origin.y, origin.z, target.x, target.y, target.z, ...))
end
-- Start continuous raycasting
---@param depth number The raycast distance
---@vararg any Additional arguments to pass to shape test
function xLib.Raycast.Start(depth, ...)
local self = setmetatable({}, xLib.Raycast)
self.refreshRate = refreshRate
self.depth = depth
self.args = {...}
self.active = true
self._thread = CreateThread(function()
while self.active do
local target, hit, coords, normal, material, entity = xLib.Raycast.FromScreen(self.depth, unpack(self.args))
self.result = {
hit = hit,
coords = coords,
normal = normal,
material = material,
entity = entity,
}
Wait(1)
end
end)
return self
end
-- Stop raycasting
function xLib.Raycast:Stop()
if not self and not self.active then return end
self.active = false
if self._thread then
self._thread = nil
end
end
-- Check if the raycating is active
---@return boolean
function xLib.Raycast:IsActive()
if not self and not self.active then return end
return self.active
end
return xLib.Raycast
+200
View File
@@ -0,0 +1,200 @@
--!DISCLAIMER
--[[
https://github.com/overextended/ox_lib
This file is licensed under LGPL-3.0 or higher <https://www.gnu.org/licenses/lgpl-3.0.en.html>
Copyright © 2025 Linden <https://github.com/thelindat>
]]
local LOADED <const> = {}
local TEMP_DATA <const> = {}
local _require = require --original lua function
package = {
path = './?.lua;./?/init.lua',
preload = {},
loaded = setmetatable({}, {
__index = LOADED,
__newindex = Noop,
__metatable = false,
})
}
---Gets the resource name and the module's relative path
---@param name string -- module name
---@return string, string
local function getModuleInfo(name)
local resource = name:match('^@(.-)/.+') -- if name is for example @esx_lib/imports/require/initl.lua it will capture ESX_LIB
if resource then
return resource, name:sub(#resource + 3) -- returns the path without script name
end
local idx = 4-- call stack depth (kept slightly lower than expected depth "just in case")
--- When indexing source of 4 it returns @@esx_lib/imports/require
--- When indexing source of 5 it returns for example @@esx_test/client.lua
--- Used for identifing resource name.
while true do
local src = debug.getinfo(idx, 'S')?.source
if not src then
error(('Couldn\'t find a source for "%s"'):format(name))
end
resource = src:match('^@@([^/]+)/.+') -- returns resource name
if resource and not src:find('^@@esx_lib/imports/require') then
return resource, name
end
idx += 1
end
end
---Searcher for a accessable path
---@param name string
---@param path string
---@return string?, string? -- filename, error message
---@diagnostic disable-next-line: duplicate-set-field
function package.searchpath(name, path)
local resource, module_name = getModuleInfo(name:gsub('%.', '/'))
local tried <const> = {}
for template in path:gmatch('[^;]+') do
local file_name = template:gsub('^%./', ''):gsub('?', module_name:gsub('%.', '/') or module_name)
local file = LoadResourceFile(resource, file_name)
if file then
TEMP_DATA[1] = file
TEMP_DATA[2] = resource
return file_name
end
tried[#tried+1] = ('no file "@%s/%s"'):format(resource, file_name)
end
return nil, table.concat(tried, '\n\t')
end
---Loads module
---@param name string
---@param env? unknown
---@return function?, string?
local function loadModule(name, env)
local file_name, err = package.searchpath(name, package.path)
if file_name then
local file = TEMP_DATA[1]
local resource = TEMP_DATA[2]
table.wipe(TEMP_DATA)
return assert(load(file, ('@@%s/%s'):format(resource, file_name), 't', env or _ENV))
end
return nil, err or 'unknown error'
end
---@diagnostic disable-next-line: duplicate-doc-alias
---@alias PackageSearcher
---| fun(name: string): function loader
---| fun(name: string): nil|false , string error message
---| fun(name: string): function?, string?
---@type PackageSearcher[]
package.searchers = {
function (name)
local ok, result = pcall(_require, name)
if ok then
return result
end
return ok, result
end,
function (name)
if package.preload[name] ~= nil then
return package.preload[name]
end
return nil, ('no field package.preload["%s"]'):format(name)
end,
function(name)
return loadModule(name)
end,
}
---Loads and runs a Lua file at the given path. Unlike require, the chunk is not cached for future use.
---@param file_path string
---@param env? unknown
function xLib.load(file_path, env)
xLib.verify(file_path, 'string', true)
local result, err = loadModule(file_path, env)
if result then
return result()
end
error(('file "%s" not found\n\t%s'):format(file_path, err))
end
---Loads and decodes a json file at the given path.
---@param file_path string
function xLib.loadJson(file_path)
xLib.verify(file_path, 'string', true)
local res_source, mod_path = getModuleInfo(file_path:gsub('%.', '/'))
local res_file = LoadResourceFile(res_source, ('%s.json'):format(mod_path))
if res_file then
return json.decode(res_file)
end
error(('json file "%s" not found\n\tno file "%s/%s.json"'):format(file_path, res_source, mod_path))
end
---Loads the given module, returns any value returned by the seacher (`true` when `nil`).\
---Passing `@resourceName.modName` loads a module from a remote resource.
---@param name string
---@return unknown
function xLib.require(name)
xLib.verify(name, 'string', true)
local module = LOADED[name]
if module == 'loading' then
error(('^1circular-dependency occurred when loading module "%s"^0'):format(name))
end
if module ~= nil then return module end
LOADED[name] = 'loading'
local err = {}
for i = 1, #package.searchers do
local result, err_msg = package.searchers[i](name)
if result then
if type(result) == 'function' then result = result() end
LOADED[name] = result or result == nil
return LOADED[name]
end
err[#err + 1] = err_msg
end
error(('%s'):format(table.concat(err, '\n\t')))
end
return xLib.require
@@ -1,22 +1,12 @@
ESX.Scaleform = {}
ESX.Scaleform.Utils = {}
---@class scaleformlib
xLib.scaleform = {}
xLib.scaleform.utils = {}
function ESX.Scaleform.ShowFreemodeMessage(title, msg, sec)
local scaleform = ESX.Scaleform.Utils.RunMethod("MP_BIG_MESSAGE_FREEMODE", "SHOW_SHARD_WASTED_MP_MESSAGE", false, title, msg)
local endTime = GetGameTimer() + (sec * 1000)
while GetGameTimer() < endTime do
Wait(0)
DrawScaleformMovieFullscreen(scaleform, 255, 255, 255, 255, 0)
end
SetScaleformMovieAsNoLongerNeeded(scaleform)
end
function ESX.Scaleform.ShowBreakingNews(title, msg, bottom, sec)
local scaleform = ESX.Scaleform.Utils.RunMethod("BREAKING_NEWS", "SET_TEXT", false, msg, bottom)
ESX.Scaleform.Utils.RunMethod(scaleform, "SET_SCROLL_TEXT", false, 0, 0, title)
ESX.Scaleform.Utils.RunMethod(scaleform, "DISPLAY_SCROLL_TEXT", false, 0, 0)
---@param title string
---@param msg string
---@param sec number
function xLib.scaleform.showFreemodeMessage(title, msg, sec)
local scaleform = xLib.scaleform.utils.runMethod("MP_BIG_MESSAGE_FREEMODE", "SHOW_SHARD_WASTED_MP_MESSAGE", false, title, msg)
local endTime = GetGameTimer() + (sec * 1000)
while GetGameTimer() < endTime do
@@ -27,8 +17,14 @@ function ESX.Scaleform.ShowBreakingNews(title, msg, bottom, sec)
SetScaleformMovieAsNoLongerNeeded(scaleform)
end
function ESX.Scaleform.ShowPopupWarning(title, msg, bottom, sec)
local scaleform = ESX.Scaleform.Utils.RunMethod("POPUP_WARNING", "SHOW_POPUP_WARNING", false, 500.0, title, msg, bottom, true)
---@param title string
---@param msg string
---@param bottom string
---@param sec number
function xLib.scaleform.showBreakingNews(title, msg, bottom, sec)
local scaleform = xLib.scaleform.utils.runMethod("BREAKING_NEWS", "SET_TEXT", false, msg, bottom)
xLib.scaleform.utils.runMethod(scaleform, "SET_SCROLL_TEXT", false, 0, 0, title)
xLib.scaleform.utils.runMethod(scaleform, "DISPLAY_SCROLL_TEXT", false, 0, 0)
local endTime = GetGameTimer() + (sec * 1000)
while GetGameTimer() < endTime do
@@ -39,8 +35,12 @@ function ESX.Scaleform.ShowPopupWarning(title, msg, bottom, sec)
SetScaleformMovieAsNoLongerNeeded(scaleform)
end
function ESX.Scaleform.ShowTrafficMovie(sec)
local scaleform = ESX.Scaleform.Utils.RunMethod("TRAFFIC_CAM", "PLAY_CAM_MOVIE", false)
---@param title string
---@param msg string
---@param bottom string
---@param sec number
function xLib.scaleform.showPopupWarning(title, msg, bottom, sec)
local scaleform = xLib.scaleform.utils.runMethod("POPUP_WARNING", "SHOW_POPUP_WARNING", false, 500.0, title, msg, bottom, true)
local endTime = GetGameTimer() + (sec * 1000)
while GetGameTimer() < endTime do
@@ -51,7 +51,22 @@ function ESX.Scaleform.ShowTrafficMovie(sec)
SetScaleformMovieAsNoLongerNeeded(scaleform)
end
function ESX.Scaleform.Utils.RequestScaleformMovie(movie)
---@param sec number
function xLib.scaleform.showTrafficMovie(sec)
local scaleform = xLib.scaleform.utils.runMethod("TRAFFIC_CAM", "PLAY_CAM_MOVIE", false)
local endTime = GetGameTimer() + (sec * 1000)
while GetGameTimer() < endTime do
Wait(0)
DrawScaleformMovieFullscreen(scaleform, 255, 255, 255, 255, 0)
end
SetScaleformMovieAsNoLongerNeeded(scaleform)
end
---@param movie string
---@return number
function xLib.scaleform.utils.requestScaleformMovie(movie)
local scaleform = RequestScaleformMovie(movie)
while not HasScaleformMovieLoaded(scaleform) do
@@ -68,8 +83,11 @@ end
---@param returnValue? boolean # Whether to return the value from the method
---@param ... number|string|boolean # Arguments to pass to the method
---@return number, number? # The scaleform handle, and the return value if `returnValue` is true
function ESX.Scaleform.Utils.RunMethod(scaleform, methodName, returnValue, ...)
scaleform = type(scaleform) == "number" and scaleform or ESX.Scaleform.Utils.RequestScaleformMovie(scaleform)
function xLib.scaleform.utils.runMethod(scaleform, methodName, returnValue, ...)
if type(scaleform) ~= "number" then
scaleform = xLib.scaleform.utils.requestScaleformMovie(scaleform)
end
BeginScaleformMovieMethod(scaleform, methodName)
local args = { ... }
@@ -97,3 +115,5 @@ function ESX.Scaleform.Utils.RunMethod(scaleform, methodName, returnValue, ...)
return scaleform
end
return xLib.scaleform
@@ -1,9 +1,10 @@
ESX.Streaming = {}
---@class streaminglib
xLib.streaming = {}
---@param modelHash number | string
---@param cb? function
---@return number | nil
function ESX.Streaming.RequestModel(modelHash, cb)
xLib.streaming.requestModel = function(modelHash, cb)
modelHash = type(modelHash) == "number" and modelHash or joaat(modelHash)
if not IsModelInCdimage(modelHash) then return end
@@ -17,7 +18,7 @@ end
---@param textureDict string
---@param cb? function
---@return string | nil
function ESX.Streaming.RequestStreamedTextureDict(textureDict, cb)
xLib.streaming.requestStreamedTextureDict = function(textureDict, cb)
RequestStreamedTextureDict(textureDict, false)
while not HasStreamedTextureDictLoaded(textureDict) do Wait(500) end
@@ -28,7 +29,7 @@ end
---@param assetName string
---@param cb? function
---@return string | nil
function ESX.Streaming.RequestNamedPtfxAsset(assetName, cb)
xLib.streaming.requestNamedPtfxAsset = function(assetName, cb)
RequestNamedPtfxAsset(assetName)
while not HasNamedPtfxAssetLoaded(assetName) do Wait(500) end
@@ -39,7 +40,7 @@ end
---@param animSet string
---@param cb? function
---@return string | nil
function ESX.Streaming.RequestAnimSet(animSet, cb)
xLib.streaming.requestAnimSet = function(animSet, cb)
RequestAnimSet(animSet)
while not HasAnimSetLoaded(animSet) do Wait(500) end
@@ -50,7 +51,7 @@ end
---@param animDict string
---@param cb? function
---@return string | nil
function ESX.Streaming.RequestAnimDict(animDict, cb)
xLib.streaming.requestAnimDict = function(animDict, cb)
RequestAnimDict(animDict)
while not HasAnimDictLoaded(animDict) do Wait(500) end
@@ -61,10 +62,24 @@ end
---@param weaponHash number | string
---@param cb? function
---@return string | number | nil
function ESX.Streaming.RequestWeaponAsset(weaponHash, cb)
xLib.streaming.requestWeaponAsset = function(weaponHash, cb)
RequestWeaponAsset(weaponHash, 31, 0)
while not HasWeaponAssetLoaded(weaponHash) do Wait(500) end
return cb and cb(weaponHash) or weaponHash
end
---@param bankName string
---@param cb? function
---@return string | nil
xLib.streaming.requestAudioBank = function(bankName, cb)
RequestAudioBank(bankName, false)
while not RequestScriptAudioBank(bankName, false) do Wait(500) end
return cb and cb(bankName) or bankName
end
return xLib.streaming
+147
View File
@@ -0,0 +1,147 @@
---@class stringlib
xLib.string = string
--- Normalize (trim + lowercase)
---@param s string
---@return string
function xLib.string.normalize(s)
xLib.verify(s, 'string', true)
local res = s:match("^%s*(.-)%s*$"):lower()
return res
end
---@param s string
---@return string
function xLib.string.capitalize(s)
xLib.verify(s, 'string', true)
local res = s:gsub("^%l", string.upper)
return res
end
---@param s string
---@return string
function xLib.string.toSnake(s)
xLib.verify(s, 'string', true)
local res = s:gsub("%s+", "_"):gsub("([a-z%d])([A-Z])", "%1_%2"):lower()
return res
end
---@param s string
---@return string
function xLib.string.toCamel(s)
xLib.verify(s, 'string', true)
local res = s:lower():gsub("_%a", function(w) return w:sub(2):upper() end)
return res
end
---@param s string
---@return string
function xLib.string.toPascal(s)
xLib.verify(s, 'string', true)
local res = s:gsub("(%a)([%w_]*)", function(first, rest)
return first:upper() .. rest:lower()
end):gsub("_", "")
return res
end
---@param s string
---@return string
function xLib.string.escapePattern(s)
xLib.verify(s, 'string', true)
local res = s:gsub("([%%%^%$%(%)%.%[%]%*%+%-%?])", "%%%1")
return res
end
---@param s string
---@param pattern string
---@return boolean
function xLib.string.matchSafe(s, pattern)
local ok, result = pcall(string.match, s, pattern)
return ok and result ~= nil
end
---@param s string
---@param pattern string
---@return string
function xLib.string.before(s, pattern)
xLib.verify(s, 'string', true)
local start = s:find(pattern, 1, true)
if not start then return s end
return s:sub(1, start - 1)
end
---@param s string
---@param pattern string
---@return string
function xLib.string.after(s, pattern)
xLib.verify(s, 'string', true)
local _, finish = s:find(pattern, 1, true)
if not finish then return s end
return s:sub(finish + 1)
end
---@param s string
---@param substr string
---@return boolean
function xLib.string.contains(s, substr)
xLib.verify(s, 'string', true)
return s:find(substr, 1, true) ~= nil
end
---@param s string
---@param old string
---@param new string
---@return string
function xLib.string.replace(s, old, new)
xLib.verify(s, 'string', true)
local result = s:gsub(xLib.string.escapePattern(old), new)
return result
end
---@param length number
---@return string
function xLib.string.randomHex(length)
local t = {}
for _ = 1, length do
t[#t + 1] = string.format("%x", math.random(0, 15))
end
return table.concat(t)
end
---@return string
function xLib.string.uuid()
local template = "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx"
local uuid = template:gsub("[xy]", function(c)
local v = (c == "x") and math.random(0, 15) or math.random(8, 11)
return string.format("%x", v)
end)
return uuid
end
return xLib.string
+359
View File
@@ -0,0 +1,359 @@
---@class xtable : tablelib
xLib.table = setmetatable({}, { __index = table })
---@param tbl table
---@return boolean
function xLib.table.isArray(tbl)
xLib.verify(tbl, "table", true)
local count, maxIndex = 0, 0
for k, _ in pairs(tbl) do
if type(k) ~= "number" or k < 1 or k % 1 ~= 0 then
return false
end
count, maxIndex = count + 1, math.max(maxIndex, k)
if count > maxIndex then
return false
end
end
return true
end
---@param tbl table
---@param item any
---@return any
function xLib.table.searchForKey(tbl, item)
xLib.verify(tbl, "table", true)
for k, v in pairs(tbl) do
if v == item then
return k
end
end
return nil
end
---@param tbl table
---@param item any
---@return boolean
function xLib.table.contains(tbl, item)
return xLib.table.searchForKey(tbl, item) ~= nil
end
---@param tbl table
---@param filter fun(value:any, key:any):boolean
---@return table
function xLib.table.filter(tbl, filter)
xLib.verify(tbl, "table", true)
xLib.verify(filter, "function", true)
local result = {}
for k, v in pairs(tbl) do
if filter(v, k) then
result[k] = v
end
end
return result
end
---@param tbl table
---@param copies? table
---@return table
function xLib.table.deepcopy(tbl, copies)
xLib.verify(tbl, "table", true)
copies = copies or {}
if copies[tbl] then
return copies[tbl]
end
local copy = {}
copies[tbl] = copy
for k, v in pairs(tbl) do
copy[k] = type(v) == "table" and xLib.table.deepcopy(v, copies) or v
if type(v) == "table" and getmetatable(v) then
setmetatable(copy[k], getmetatable(v))
end
end
return copy
end
--- https://github.com/overextended/ox_lib/blob/master/imports/table/shared.lua
---@param t1 table
---@param t2 table
---@param addDuplicateNumbers boolean
---@return table
function xLib.table.merge(t1, t2, addDuplicateNumbers)
xLib.verify(t1, "table", true)
xLib.verify(t2, "table", true)
xLib.verify(addDuplicateNumbers, "boolean", true)
addDuplicateNumbers = addDuplicateNumbers == nil or addDuplicateNumbers
for k, v2 in pairs(t2) do
local v1 = t1[k]
local type1 = type(v1)
local type2 = type(v2)
if type1 == 'table' and type2 == 'table' then
xLib.table.merge(v1, v2, addDuplicateNumbers)
elseif addDuplicateNumbers and (type1 == 'number' and type2 == 'number') then
t1[k] = v1 + v2
else
t1[k] = v2
end
end
return t1
end
function xLib.table.dump(tbl)
if xLib.verify(tbl, 'table') then
local s = '{ '
for k,v in pairs(tbl) do
if type(k) ~= 'number' then k = '"'..k..'"' end
s = s .. '['..k..'] = ' .. tbl(v) .. ','
end
return s .. '} '
else
return tostring(tbl)
end
end
-- nil proof alternative to #table
---@param t table
---@return number
function xLib.table.sizeOf(t)
local count = 0
for _, _ in pairs(t) do
count = count + 1
end
return count
end
---@param t table
---@return table
function xLib.table.set(t)
local set = {}
for _, v in ipairs(t) do
set[v] = true
end
return set
end
---@param t table
---@param value any
---@return number
function xLib.table.indexOf(t, value)
for i = 1, #t, 1 do
if t[i] == value then
return i
end
end
return -1
end
---@param t table
---@param value any
---@return number
function xLib.table.lastIndexOf(t, value)
for i = #t, 1, -1 do
if t[i] == value then
return i
end
end
return -1
end
---@param t table
---@param cb function
---@return any
function xLib.table.find(t, cb)
for i = 1, #t, 1 do
if cb(t[i]) then
return t[i]
end
end
return nil
end
---@param t table
---@param cb function
---@return number
function xLib.table.findIndex(t, cb)
for i = 1, #t, 1 do
if cb(t[i]) then
return i
end
end
return -1
end
---@param t table
---@param cb function
---@return table
function xLib.table.map(t, cb)
local newTable = {}
for i = 1, #t, 1 do
newTable[i] = cb(t[i], i)
end
return newTable
end
---@param t table
---@return table
function xLib.table.reverse(t)
local newTable = {}
for i = #t, 1, -1 do
table.insert(newTable, t[i])
end
return newTable
end
---@param t table
---@return table
function xLib.table.clone(t)
if type(t) ~= "table" then
return t
end
local meta = getmetatable(t)
local target = {}
for k, v in pairs(t) do
if type(v) == "table" then
target[k] = xLib.table.clone(v)
else
target[k] = v
end
end
setmetatable(target, meta)
return target
end
---@param t1 table
---@param t2 table
---@return table
function xLib.table.concat(t1, t2)
local t3 = xLib.table.clone(t1)
for i = 1, #t2, 1 do
table.insert(t3, t2[i])
end
return t3
end
---@param t table
---@param sep string
---@return string
function xLib.table.join(t, sep)
local str = ""
for i = 1, #t, 1 do
if i > 1 then
str = str .. (sep or ",")
end
str = str .. t[i]
end
return str
end
-- Credits: https://github.com/JonasDev99/qb-garages/blob/b0335d67cb72a6b9ac60f62a87fb3946f5c2f33d/server/main.lua#L5
---@param tab table
---@param val any
---@return boolean
function xLib.table.contains(tab, val)
if type(val) == "table" then
for _, value in pairs(tab) do
if xLib.table.contains(val, value) then
return true
end
end
return false
else
for _, value in pairs(tab) do
if value == val then
return true
end
end
end
return false
end
-- Credit: https://stackoverflow.com/a/15706820
-- Description: sort function for pairs
---@param t table
---@param order function
---@return function
function xLib.table.sort(t, order)
-- collect the keys
local keys = {}
for k, _ in pairs(t) do
keys[#keys + 1] = k
end
-- if order function given, sort by it by passing the table and keys a, b,
-- otherwise just sort the keys
if order then
table.sort(keys, function(a, b)
return order(t, a, b)
end)
else
table.sort(keys)
end
-- return the iterator function
local i = 0
return function()
i = i + 1
if keys[i] then
return keys[i], t[keys[i]]
end
end
end
---@param t table
---@return Array
function xLib.table.toArray(t)
local array = {}
for _, v in pairs(t) do
array[#array + 1] = v
end
return array
end
---@param t table
---@return table
function xLib.table.wipe(t)
return table.wipe(t)
end
return xLib.table
@@ -1,10 +1,14 @@
xLib.timeout = {}
local TimeoutCount = 0
local CancelledTimeouts = {}
---@param msec number
---@param cb function
---@return number
ESX.SetTimeout = function(msec, cb)
xLib.timeout.setTimeout = function(msec, cb)
xLib.verify(cb, "function", true)
local id <const> = TimeoutCount + 1
SetTimeout(msec, function()
@@ -12,7 +16,6 @@ ESX.SetTimeout = function(msec, cb)
CancelledTimeouts[id] = nil
return
end
cb()
end)
@@ -23,6 +26,8 @@ end
---@param id number
---@return nil
ESX.ClearTimeout = function(id)
xLib.timeout.clearTimeout = function(id)
CancelledTimeouts[id] = true
end
return xLib.timeout
+161
View File
@@ -0,0 +1,161 @@
---@alias CustomType 'number' | 'boolean' | 'function' | 'table' | 'string' | 'nil' | 'array' | 'int' | 'uint' | 'float' | 'char' | 'vector3' | 'vector4' | 'ped' | 'playerId' | 'vehicle' | 'prop' | 'class' | 'model'
---Checks if value is an array
---@param value any
---@return boolean
local function isArray(value)
if type(value) ~= 'table' then
return false
end
local i = 0
for _ in pairs(value) do
i = i + 1
if value[i] == nil then
return false
end
end
return true
end
---Checks if player id is correct.
---@param value any
---@return boolean
local function isPlayerId(value)
if type(value) ~= 'number' then
return false
end
if xLib.side == 'server' then
return GetPlayerName(value) ~= nil
else
return NetworkIsPlayerActive(value)
end
end
---Checks if function is callable.
---@param value any
---@return boolean
local function isCallable(value)
local value_type = type(value)
if value_type == 'function' then
return true
end
if value_type == 'table' then
local mt = getmetatable(value)
if mt and mt.__call then
return true
end
end
return false
end
xLib.callback.register('xLib:validateModel', function(model)
return IsModelValid(model)
end)
local function validateModel(model)
local players = GetPlayers()
local source = tonumber(players[math.random(1,#players)])
return xLib.callback.await('xLib:validateModel', source, false, model)
end
---Make sure value is a valid type
---@param value any
---@param valid_type CustomType
---@return boolean
local function verifyType(value, valid_type)
if valid_type == 'function' then
return isCallable(value)
elseif valid_type == 'array' then
return isArray(value)
elseif valid_type == 'int' then
return math.type(value) == 'integer'
elseif valid_type == 'float' then
return math.type(value) == 'float'
elseif valid_type == 'uint' then
return math.type(value) == 'int' and value >= 0
elseif valid_type == 'char' then
return type(value) == 'string' and #value == 1
elseif valid_type == 'ped' then
return GetEntityType(value) == 1
elseif valid_type == 'vehicle' then
return GetEntityType(value) == 2
elseif valid_type == 'prop' then
return GetEntityType(value) == 3
elseif valid_type == 'playerId' then
return isPlayerId(value)
elseif valid_type == 'class' then
if type(value) ~= 'table' then
return false
end
return getmetatable(value)?._isClass == true
elseif valid_type == 'model' then
if xLib.side == "server" then
return validateModel(value)
end
return IsModelValid(value)
end
return type(value) == valid_type
end
---@class VerifyTable
---@field debug VerifyFunction
---@alias VerifyFunction fun(value: any, valid_types: CustomType[] | CustomType, throw_error?: boolean): boolean
---@type VerifyFunction | VerifyTable
xLib.verify = setmetatable({
fn = function(value, valid_types, throw_error)
local match = false
if type(valid_types) == 'table' then
for i = 1, #valid_types do
if xLib.verify(value, valid_types[i]) then
match = true
break
end
end
if throw_error and not match then
error(('[xLib] Couldn\'t match %s to types %s'):format(value, json.encode(valid_types)))
else
return match
end
end
match = verifyType(value, valid_types)
if throw_error and not match then
error(('[xLib] Couldn\'t match %s to type %s'):format(value, valid_types))
else
return match
end
end
}, {
__index = function(self, k)
if k == 'debug' and xLib.debug then
return rawget(self, 'fn'
)
elseif k ~= 'debug' then
return rawget(self, k)
else
return Noop
end
end,
__call = function(self, ...)
return rawget(self, 'fn')(...)
end
})
return xLib.verify
+45
View File
@@ -0,0 +1,45 @@
---Yields the current thread until the callback returns a non-nil value.
---@generic T
---@param cb fun(): T?
---@param errMessage? string
---@param timeout? number | false Error out after `~x` ms if the callback hasn't resolved. Defaults to 1000, unless set to `false`.
---@param interval? integer Polling interval in ms. Defaults to 0. A value above `timeout` overshoots it by one cycle.
---@return T
---@async
function xLib.waitFor(cb, errMessage, timeout, interval)
xLib.verify(cb, 'function', true)
if interval then
xLib.verify(interval, 'int', true)
end
local value = cb()
if value ~= nil then
return value
end
if timeout ~= false and type(timeout) ~= 'number' then
timeout = 5000
end
local start = timeout and GetGameTimer()
while value == nil do
Wait(interval or 0)
value = cb()
if value == nil and timeout then
local elapsed = GetGameTimer() - start
if elapsed > timeout then
return error(('%s (waited %.1fms)'):format(errMessage or 'failed to resolve callback', elapsed), 2)
end
end
end
return value
end
return xLib.waitFor
@@ -0,0 +1,74 @@
--[[
https://github.com/overextended/ox_lib
This file is licensed under LGPL-3.0 or higher <https://www.gnu.org/licenses/lgpl-3.0.en.html>
Copyright © 2025 Linden <https://github.com/thelindat>
]]
local registeredCallbacks = {}
local resource_name = GetCurrentResourceName() --TODO: Add cache
local IS_DEBUG <const> = GetConvar("xLib:debug", "false") == "true"
AddEventHandler('onResourceStop', function(resourceName)
if resource_name == resourceName then return end
for callbackName, resource in pairs(registeredCallbacks) do
if resource == resourceName then
registeredCallbacks[callbackName] = nil
end
end
end)
---For internal use only.
---Sets a callback event as registered to a specific resource, preventing it from
---being overwritten. Any unknown callbacks will return an error to the caller.
---@param callbackName string
---@param isValid boolean
function xLib.setValidCallback(callbackName, isValid)
local resourceName = GetInvokingResource() or resource_name
local callbackResource = registeredCallbacks[callbackName]
if callbackResource then
if not isValid then
callbackResource[callbackName] = nil
return
end
if callbackResource == resourceName then return end
if IS_DEBUG then
local errMessage = ("^1resource '%s' attempted to overwrite callback '%s' owned by resource '%s'^0"):format(resourceName, callbackName, callbackResource)
print(('^1SCRIPT ERROR: %s^0\n%s'):format(errMessage,
Citizen.InvokeNative(`FORMAT_STACK_TRACE` & 0xFFFFFFFF, nil, 0, Citizen.ResultAsString()) or ''))
end
return
end
if IS_DEBUG then
print(("set valid callback '%s' for resource '%s'"):format(callbackName, resourceName))
end
registeredCallbacks[callbackName] = resourceName
end
function xLib.isCallbackValid(callbackName)
return registeredCallbacks[callbackName] == GetInvokingResource() or resource_name
end
local cbEvent = '__xLib_cb_%s'
RegisterNetEvent('xLib:validateCallback', function(callbackName, invokingResource, key)
if registeredCallbacks[callbackName] then return end
local event = cbEvent:format(invokingResource)
if GetGameName() == 'fxserver' then
return TriggerClientEvent(event, source, key, 'cb_invalid')
end
TriggerServerEvent(event, key, 'cb_invalid')
end)
+38
View File
@@ -0,0 +1,38 @@
---@diagnostic disable: lowercase-global
xLib = setmetatable({
name = 'xLib',
side = IsDuplicityVersion() and 'server' or 'client'
}, {
__newindex = function(self, key, fn)
rawset(self, key, fn)
if debug.getinfo(2, 'S').short_src:find('@esx_lib/resource') then
exports(key, fn)
end
end,
__index = function(self, key)
local dir = ('imports/%s'):format(key)
local chunk = LoadResourceFile(self.name, ('%s/%s.lua'):format(dir, self.side))
local shared = LoadResourceFile(self.name, ('%s/shared.lua'):format(dir))
if shared then
chunk = (chunk and ('%s\n%s'):format(shared, chunk)) or shared
end
if chunk then
local fn, err = load(chunk, ('@@esx_lib/%s/%s.lua'):format(key, self.side))
if not fn or err then
return error(('\n^1Error importing module (%s): %s^0'):format(dir, err), 3)
end
rawset(self, key, fn() or Noop)
return self[key]
end
end
})
require = xLib.require
+207
View File
@@ -0,0 +1,207 @@
-- xLib pub/sub: a producer subscribes players to a topic server-side and
-- publishes data pushed only to them. Clients only listen, never subscribe.
-- The registry is shared here (esx_lib server VM). Namespace topics by resource
-- (e.g. "esx_shops:247:stock") so two resources cannot clash on a bare name.
---@diagnostic disable: duplicate-set-field
local PUBSUB_EVENT <const> = '__xLib_pubsub' -- must match imports/pubsub/client.lua
local TOPIC_PATTERN <const> = '^[%w_:%.%-]+$'
local MAX_TOPIC_LEN <const> = 200
local resourceName <const> = GetCurrentResourceName()
-- topic -> set of subscribed serverIds: subs[topic][src] = true
local subs = {}
-- serverId -> set of topics: memberOf[src][topic] = true (O(degree) cleanup on drop)
local memberOf = {}
-- topic -> resource that created it, purged on that resource's onResourceStop
local ownerOf = {}
local function isValidTopic(topic)
return type(topic) == 'string'
and #topic > 0
and #topic <= MAX_TOPIC_LEN
and topic:match(TOPIC_PATTERN) ~= nil
end
local function isLivePlayer(src)
-- a connected player has a name; rejects 0, stale or recycled serverIds
return type(src) == 'number' and src > 0 and GetPlayerName(src) ~= nil
end
local function detach(src, topic)
local set = subs[topic]
if set then
set[src] = nil
if next(set) == nil then
subs[topic] = nil
ownerOf[topic] = nil
end
end
local topics = memberOf[src]
if topics then
topics[topic] = nil
if next(topics) == nil then
memberOf[src] = nil
end
end
end
---Subscribe a player to a topic. Called server-side by the producer resource.
---@param src number serverId of the player to add
---@param topic string
---@return boolean added false if already subscribed or input is invalid
function xLib.pubsub_subscribe(src, topic)
if not isLivePlayer(src) or not isValidTopic(topic) then
return false
end
local set = subs[topic]
if set and set[src] then
return false -- idempotent: already subscribed
end
if not set then
set = {}
subs[topic] = set
ownerOf[topic] = GetInvokingResource() or resourceName
end
set[src] = true
local topics = memberOf[src]
if not topics then
topics = {}
memberOf[src] = topics
end
topics[topic] = true
return true
end
---Unsubscribe a player from a topic.
---@param src number
---@param topic string
---@return boolean removed false if it was not subscribed
function xLib.pubsub_unsubscribe(src, topic)
local set = subs[topic]
if not set or not set[src] then
return false -- idempotent
end
detach(src, topic)
return true
end
---Push data to every player subscribed to a topic. Server-trusted data only.
---@param topic string
---@param data any
---@return integer count number of players notified
function xLib.pubsub_publish(topic, data)
-- no isValidTopic() here on purpose: an invalid topic never created a set,
-- so the lookup below already returns 0 for it without a regex on the hot path
local set = subs[topic]
if not set then
return 0
end
local targets, count = {}, 0
for src in pairs(set) do
count = count + 1
targets[count] = src
end
-- packs the payload once for the whole set instead of once per client
xLib.triggerClientEvent(PUBSUB_EVENT, targets, topic, data)
return count
end
---@param topic string
---@return number[] serverIds a fresh copy, empty if the topic has no subscribers
function xLib.pubsub_subscribers(topic)
local set = subs[topic]
if not set then
return {}
end
local list, i = {}, 0
for src in pairs(set) do
i = i + 1
list[i] = src
end
return list
end
---@param src number
---@param topic string
---@return boolean
function xLib.pubsub_isSubscribed(src, topic)
local set = subs[topic]
return set ~= nil and set[src] == true
end
---Drop a whole topic and all of its subscribers.
---@param topic string
---@return integer count number of subscribers that were removed
function xLib.pubsub_clear(topic)
local set = subs[topic]
if not set then
return 0
end
local count = 0
for src in pairs(set) do
local topics = memberOf[src]
if topics then
topics[topic] = nil
if next(topics) == nil then
memberOf[src] = nil
end
end
count = count + 1
end
subs[topic] = nil
ownerOf[topic] = nil
return count
end
-- Remove a leaving player from every topic. serverIds are reused by the server,
-- so skipping this would leak a topic's data to whoever inherits the slot.
AddEventHandler('playerDropped', function()
local src = source
local topics = memberOf[src]
if not topics then
return
end
for topic in pairs(topics) do
local set = subs[topic]
if set then
set[src] = nil
if next(set) == nil then
subs[topic] = nil
ownerOf[topic] = nil
end
end
end
memberOf[src] = nil
end)
-- Purge the topics a stopped producer owned, so a producer restart never leaves
-- orphan subscriptions that keep receiving pushes.
AddEventHandler('onResourceStop', function(stopped)
if stopped == resourceName then
return
end
local orphaned = {}
for topic, owner in pairs(ownerOf) do
if owner == stopped then
orphaned[#orphaned + 1] = topic
end
end
for i = 1, #orphaned do
xLib.pubsub_clear(orphaned[i])
end
end)
@@ -0,0 +1,21 @@
-- Triggers an event for one or more clients. For an array of players the payload
-- is packed once instead of being re-serialised per client.
---@diagnostic disable: duplicate-set-field
local pack = msgpack.pack_args
---@param eventName string
---@param targets number | number[] a single serverId, or an array of them
---@param ... any
function xLib.triggerClientEvent(eventName, targets, ...)
if type(targets) == 'number' then
return TriggerClientEvent(eventName, targets, ...)
end
local payload = pack(...)
local length = #payload
for i = 1, #targets do
TriggerClientEventInternal(eventName, targets[i], payload, length)
end
end