891 lines
28 KiB
Lua
891 lines
28 KiB
Lua
---@param msg string
|
|
---@return nil
|
|
function ESX.Trace(msg)
|
|
if Config.EnableDebug then
|
|
print(("[^2TRACE^7] %s^7"):format(msg))
|
|
end
|
|
end
|
|
|
|
--- Triggers an event for one or more clients.
|
|
---@param eventName string The name of the event to trigger.
|
|
---@param playerIds table|number If a number, represents a single player ID. If a table, represents an array of player IDs.
|
|
---@param ... any Additional arguments to pass to the event handler.
|
|
function ESX.TriggerClientEvent(eventName, playerIds, ...)
|
|
if type(playerIds) == "number" then
|
|
TriggerClientEvent(eventName, playerIds, ...)
|
|
return
|
|
end
|
|
|
|
local payload = msgpack.pack_args(...)
|
|
local payloadLength = #payload
|
|
|
|
for i = 1, #playerIds do
|
|
TriggerClientEventInternal(eventName, playerIds[i], payload, payloadLength)
|
|
end
|
|
end
|
|
|
|
---@param name string | table
|
|
---@param group string | table
|
|
---@param cb function
|
|
---@param allowConsole? boolean
|
|
---@param suggestion? table
|
|
function ESX.RegisterCommand(name, group, cb, allowConsole, suggestion)
|
|
if type(name) == "table" then
|
|
for _, v in ipairs(name) do
|
|
ESX.RegisterCommand(v, group, cb, allowConsole, suggestion)
|
|
end
|
|
return
|
|
end
|
|
|
|
if Core.RegisteredCommands[name] then
|
|
print(('[^3WARNING^7] Command ^5"%s" ^7already registered, overriding command'):format(name))
|
|
|
|
if Core.RegisteredCommands[name].suggestion then
|
|
TriggerClientEvent("chat:removeSuggestion", -1, ("/%s"):format(name))
|
|
end
|
|
end
|
|
|
|
if suggestion then
|
|
if not suggestion.arguments then
|
|
suggestion.arguments = {}
|
|
end
|
|
if not suggestion.help then
|
|
suggestion.help = ""
|
|
end
|
|
|
|
TriggerClientEvent("chat:addSuggestion", -1, ("/%s"):format(name), suggestion.help, suggestion.arguments)
|
|
end
|
|
|
|
Core.RegisteredCommands[name] = { group = group, cb = cb, allowConsole = allowConsole, suggestion = suggestion }
|
|
|
|
RegisterCommand(name, function(playerId, args)
|
|
local command = Core.RegisteredCommands[name]
|
|
|
|
if not command.allowConsole and playerId == 0 then
|
|
print(("[^3WARNING^7] ^5%s^0"):format(TranslateCap("commanderror_console")))
|
|
else
|
|
local xPlayer, err = ESX.Players[playerId], nil
|
|
|
|
if command.suggestion then
|
|
if command.suggestion.validate then
|
|
if #args ~= #command.suggestion.arguments then
|
|
err = TranslateCap("commanderror_argumentmismatch", #args, #command.suggestion.arguments)
|
|
end
|
|
end
|
|
|
|
if not err and command.suggestion.arguments then
|
|
local newArgs = {}
|
|
|
|
for k, v in ipairs(command.suggestion.arguments) do
|
|
if v.type then
|
|
if v.type == "number" then
|
|
local newArg = tonumber(args[k])
|
|
|
|
if newArg then
|
|
newArgs[v.name] = newArg
|
|
else
|
|
err = TranslateCap("commanderror_argumentmismatch_number", k)
|
|
end
|
|
elseif v.type == "player" or v.type == "playerId" then
|
|
local targetPlayer = tonumber(args[k])
|
|
|
|
if args[k] == "me" then
|
|
targetPlayer = playerId
|
|
end
|
|
|
|
if targetPlayer then
|
|
local xTargetPlayer = ESX.GetPlayerFromId(targetPlayer)
|
|
|
|
if xTargetPlayer then
|
|
if v.type == "player" then
|
|
newArgs[v.name] = xTargetPlayer
|
|
else
|
|
newArgs[v.name] = targetPlayer
|
|
end
|
|
else
|
|
err = TranslateCap("commanderror_invalidplayerid")
|
|
end
|
|
else
|
|
err = TranslateCap("commanderror_argumentmismatch_number", k)
|
|
end
|
|
elseif v.type == "string" then
|
|
local newArg = tonumber(args[k])
|
|
if not newArg then
|
|
newArgs[v.name] = args[k]
|
|
else
|
|
err = TranslateCap("commanderror_argumentmismatch_string", k)
|
|
end
|
|
elseif v.type == "item" then
|
|
if ESX.Items[args[k]] then
|
|
newArgs[v.name] = args[k]
|
|
else
|
|
err = TranslateCap("commanderror_invaliditem")
|
|
end
|
|
elseif v.type == "weapon" then
|
|
if ESX.GetWeapon(args[k]) then
|
|
newArgs[v.name] = string.upper(args[k])
|
|
else
|
|
err = TranslateCap("commanderror_invalidweapon")
|
|
end
|
|
elseif v.type == "any" then
|
|
newArgs[v.name] = args[k]
|
|
elseif v.type == "merge" then
|
|
local length = 0
|
|
for i = 1, k - 1 do
|
|
length = length + string.len(args[i]) + 1
|
|
end
|
|
local merge = table.concat(args, " ")
|
|
|
|
newArgs[v.name] = string.sub(merge, length)
|
|
elseif v.type == "coordinate" then
|
|
local coord = tonumber(args[k]:match("(-?%d+%.?%d*)"))
|
|
if not coord then
|
|
err = TranslateCap("commanderror_argumentmismatch_number", k)
|
|
else
|
|
newArgs[v.name] = coord
|
|
end
|
|
end
|
|
end
|
|
|
|
if ESX.IsFunctionReference(v.Validator?.validate) and not err then
|
|
local candidate = newArgs[v.name]
|
|
local ok, res = pcall(v.Validator.validate, candidate)
|
|
if not ok or res ~= true then
|
|
err = v.Validator.err or TranslateCap("commanderror_argumentmismatch")
|
|
end
|
|
end
|
|
|
|
--backwards compatibility
|
|
if v.validate ~= nil and not v.validate then
|
|
err = nil
|
|
end
|
|
|
|
if err then
|
|
break
|
|
end
|
|
end
|
|
|
|
args = newArgs
|
|
end
|
|
end
|
|
|
|
if err then
|
|
if playerId == 0 then
|
|
print(("[^3WARNING^7] %s^7"):format(err))
|
|
else
|
|
xPlayer.showNotification(err)
|
|
end
|
|
else
|
|
cb(xPlayer or false, args, function(msg)
|
|
if playerId == 0 then
|
|
print(("[^3WARNING^7] %s^7"):format(msg))
|
|
else
|
|
xPlayer.showNotification(msg)
|
|
end
|
|
end)
|
|
end
|
|
end
|
|
end, true)
|
|
|
|
if type(group) == "table" then
|
|
for _, v in ipairs(group) do
|
|
ExecuteCommand(("add_ace group.%s command.%s allow"):format(v, name))
|
|
end
|
|
else
|
|
ExecuteCommand(("add_ace group.%s command.%s allow"):format(group, name))
|
|
end
|
|
end
|
|
|
|
local function updateHealthAndArmorInMetadata(xPlayer)
|
|
local ped = GetPlayerPed(xPlayer.source)
|
|
xPlayer.setMeta("health", GetEntityHealth(ped))
|
|
xPlayer.setMeta("armor", GetPedArmour(ped))
|
|
xPlayer.setMeta("lastPlaytime", xPlayer.getPlayTime())
|
|
end
|
|
|
|
---@param xPlayer table
|
|
---@param cb? function
|
|
---@return nil
|
|
function Core.SavePlayer(xPlayer, cb)
|
|
if not xPlayer.spawned then
|
|
return cb and cb()
|
|
end
|
|
|
|
updateHealthAndArmorInMetadata(xPlayer)
|
|
local parameters <const> = {
|
|
json.encode(xPlayer.getAccounts(true)),
|
|
xPlayer.job.name,
|
|
xPlayer.job.grade,
|
|
xPlayer.group,
|
|
json.encode(xPlayer.getCoords()),
|
|
json.encode(xPlayer.getInventory(true)),
|
|
json.encode(xPlayer.getLoadout(true)),
|
|
json.encode(xPlayer.getMeta()),
|
|
xPlayer.identifier,
|
|
}
|
|
exports['codem-inventory']:SaveInventory(xPlayer.source)
|
|
MySQL.prepare(
|
|
"UPDATE `users` SET `accounts` = ?, `job` = ?, `job_grade` = ?, `group` = ?, `position` = ?, `inventory` = ?, `loadout` = ?, `metadata` = ? WHERE `identifier` = ?",
|
|
parameters,
|
|
function(affectedRows)
|
|
if affectedRows == 1 then
|
|
print(('[^2INFO^7] Saved player ^5"%s^7"'):format(xPlayer.name))
|
|
TriggerEvent("esx:playerSaved", xPlayer.playerId, xPlayer)
|
|
end
|
|
if cb then
|
|
cb()
|
|
end
|
|
end
|
|
)
|
|
end
|
|
|
|
---@param cb? function
|
|
---@return nil
|
|
function Core.SavePlayers(cb)
|
|
local xPlayers <const> = ESX.Players
|
|
if not next(xPlayers) then
|
|
return
|
|
end
|
|
|
|
local startTime <const> = os.time()
|
|
local parameters = {}
|
|
|
|
for _, xPlayer in pairs(ESX.Players) do
|
|
updateHealthAndArmorInMetadata(xPlayer)
|
|
parameters[#parameters + 1] = {
|
|
json.encode(xPlayer.getAccounts(true)),
|
|
xPlayer.job.name,
|
|
xPlayer.job.grade,
|
|
xPlayer.group,
|
|
json.encode(xPlayer.getCoords(false, true)),
|
|
json.encode(xPlayer.getInventory(true)),
|
|
json.encode(xPlayer.getLoadout(true)),
|
|
json.encode(xPlayer.getMeta()),
|
|
xPlayer.identifier,
|
|
}
|
|
end
|
|
|
|
MySQL.prepare(
|
|
"UPDATE `users` SET `accounts` = ?, `job` = ?, `job_grade` = ?, `group` = ?, `position` = ?, `inventory` = ?, `loadout` = ?, `metadata` = ? WHERE `identifier` = ?",
|
|
parameters,
|
|
function(results)
|
|
if not results then
|
|
return
|
|
end
|
|
|
|
if type(cb) == "function" then
|
|
return cb()
|
|
end
|
|
|
|
print(("[^2INFO^7] Saved ^5%s^7 %s over ^5%s^7 ms"):format(#parameters,
|
|
#parameters > 1 and "players" or "player", ESX.Math.Round((os.time() - startTime) / 1000000, 2)))
|
|
end
|
|
)
|
|
end
|
|
|
|
ESX.GetPlayers = GetPlayers
|
|
|
|
local function checkTable(key, val, xPlayer, xPlayers, minimal)
|
|
for valIndex = 1, #val do
|
|
local value = val[valIndex]
|
|
if not xPlayers[value] then
|
|
xPlayers[value] = {}
|
|
end
|
|
|
|
if (key == "job" and xPlayer.job.name == value) or xPlayer[key] == value then
|
|
xPlayers[value][#xPlayers[value] + 1] = (minimal and xPlayer.source or xPlayer)
|
|
end
|
|
end
|
|
end
|
|
|
|
---@param key? string
|
|
---@param val? string|table
|
|
---@param minimal? boolean
|
|
---@return xPlayer[]|number[]|table<any, xPlayer[]>|table<any, number[]>
|
|
function ESX.GetExtendedPlayers(key, val, minimal)
|
|
if not key then
|
|
if not minimal then
|
|
return ESX.Table.ToArray(ESX.Players)
|
|
end
|
|
|
|
local xPlayers = {}
|
|
local index = 1
|
|
for src, _ in pairs(ESX.Players) do
|
|
xPlayers[index] = src
|
|
index += 1
|
|
end
|
|
|
|
return xPlayers
|
|
end
|
|
|
|
local xPlayers = {}
|
|
if type(val) == "table" then
|
|
for _, xPlayer in pairs(ESX.Players) do
|
|
checkTable(key, val, xPlayer, xPlayers, minimal)
|
|
end
|
|
|
|
return xPlayers
|
|
end
|
|
|
|
for _, xPlayer in pairs(ESX.Players) do
|
|
if (key == "job" and xPlayer.job.name == val) or xPlayer[key] == val then
|
|
xPlayers[#xPlayers + 1] = (minimal and xPlayer.source or xPlayer)
|
|
end
|
|
end
|
|
|
|
return xPlayers
|
|
end
|
|
|
|
---@param key? string
|
|
---@param val? string|table
|
|
---@return number | table
|
|
function ESX.GetNumPlayers(key, val)
|
|
if not key then
|
|
return #GetPlayers()
|
|
end
|
|
|
|
if type(val) == "table" then
|
|
local numPlayers = {}
|
|
if key == "job" then
|
|
for _, v in ipairs(val) do
|
|
numPlayers[v] = (Core.JobsPlayerCount[v] or 0)
|
|
end
|
|
return numPlayers
|
|
end
|
|
|
|
local filteredPlayers = ESX.GetExtendedPlayers(key, val)
|
|
for i, v in pairs(filteredPlayers) do
|
|
numPlayers[i] = (#v or 0)
|
|
end
|
|
return numPlayers
|
|
end
|
|
|
|
if key == "job" then
|
|
return (Core.JobsPlayerCount[val] or 0)
|
|
end
|
|
|
|
return #ESX.GetExtendedPlayers(key, val)
|
|
end
|
|
|
|
---@param source number
|
|
---@return xPlayer?
|
|
function ESX.GetPlayerFromId(source)
|
|
return ESX.Players[tonumber(source)]
|
|
end
|
|
|
|
---@param identifier string
|
|
---@return xPlayer?
|
|
function ESX.GetPlayerFromIdentifier(identifier)
|
|
return Core.playersByIdentifier[identifier]
|
|
end
|
|
|
|
---@param identifier string
|
|
---@return number playerId
|
|
function ESX.GetPlayerIdFromIdentifier(identifier)
|
|
return Core.playersByIdentifier[identifier]?.source
|
|
end
|
|
|
|
---@param source number
|
|
---@return boolean
|
|
---@diagnostic disable-next-line: duplicate-set-field
|
|
function ESX.IsPlayerLoaded(source)
|
|
return ESX.Players[source] ~= nil
|
|
end
|
|
|
|
---@param playerId number | string
|
|
---@return string, number
|
|
function ESX.GetIdentifier(playerId)
|
|
local fxDk = GetConvarInt("sv_fxdkMode", 0)
|
|
if fxDk == 1 then
|
|
return "ESX-DEBUG-LICENCE", 0
|
|
end
|
|
|
|
playerId = tostring(playerId)
|
|
|
|
local identifierType = Config.Identifier
|
|
local identifier = GetPlayerIdentifierByType(playerId, identifierType)
|
|
|
|
assert(identifier, ("[ESX] GetIdentifier failed: no identifier found for playerId %s with type '%s'"):format(playerId, identifierType))
|
|
|
|
return identifier:gsub(("%s:"):format(identifierType), "")
|
|
end
|
|
|
|
---@param model string|number
|
|
---@param player number
|
|
---@param cb function?
|
|
---@return string?
|
|
---@diagnostic disable-next-line: duplicate-set-field
|
|
function ESX.GetVehicleType(model, player, cb)
|
|
if cb and not ESX.IsFunctionReference(cb) then
|
|
error("Invalid callback function")
|
|
end
|
|
|
|
local promise = not cb and promise.new()
|
|
local function resolve(result)
|
|
if promise then
|
|
promise:resolve(result)
|
|
elseif cb then
|
|
cb(result)
|
|
end
|
|
|
|
return result
|
|
end
|
|
|
|
model = type(model) == "string" and joaat(model) or model
|
|
|
|
if Core.vehicleTypesByModel[model] then
|
|
return resolve(Core.vehicleTypesByModel[model])
|
|
end
|
|
|
|
ESX.TriggerClientCallback(player, "esx:GetVehicleType", function(vehicleType)
|
|
Core.vehicleTypesByModel[model] = vehicleType
|
|
resolve(vehicleType)
|
|
end, model)
|
|
|
|
if promise then
|
|
return Citizen.Await(promise)
|
|
end
|
|
end
|
|
|
|
---@param name string
|
|
---@param title string
|
|
---@param color string
|
|
---@param message string
|
|
---@return nil
|
|
function ESX.DiscordLog(name, title, color, message)
|
|
local webHook = Config.DiscordLogs.Webhooks[name] or Config.DiscordLogs.Webhooks.default
|
|
local embedData = {
|
|
{
|
|
["title"] = title,
|
|
["color"] = Config.DiscordLogs.Colors[color] or Config.DiscordLogs.Colors.default,
|
|
["footer"] = {
|
|
["text"] = "| ESX Logs | " .. os.date(),
|
|
["icon_url"] =
|
|
"https://cdn.discordapp.com/attachments/944789399852417096/1020099828266586193/blanc-800x800.png",
|
|
},
|
|
["description"] = message,
|
|
["author"] = {
|
|
["name"] = "ESX Framework",
|
|
["icon_url"] = "https://cdn.discordapp.com/emojis/939245183621558362.webp?size=128&quality=lossless",
|
|
},
|
|
},
|
|
}
|
|
PerformHttpRequest(
|
|
webHook,
|
|
function()
|
|
return
|
|
end,
|
|
"POST",
|
|
json.encode({
|
|
username = "Logs",
|
|
embeds = embedData,
|
|
}),
|
|
{
|
|
["Content-Type"] = "application/json",
|
|
}
|
|
)
|
|
end
|
|
|
|
---@param name string
|
|
---@param title string
|
|
---@param color string
|
|
---@param fields table
|
|
---@return nil
|
|
function ESX.DiscordLogFields(name, title, color, fields)
|
|
for i = 1, #fields do
|
|
local field = fields[i]
|
|
field.value = tostring(field.value)
|
|
end
|
|
|
|
local webHook = Config.DiscordLogs.Webhooks[name] or Config.DiscordLogs.Webhooks.default
|
|
local embedData = {
|
|
{
|
|
["title"] = title,
|
|
["color"] = Config.DiscordLogs.Colors[color] or Config.DiscordLogs.Colors.default,
|
|
["footer"] = {
|
|
["text"] = "| ESX Logs | " .. os.date(),
|
|
["icon_url"] =
|
|
"https://cdn.discordapp.com/attachments/944789399852417096/1020099828266586193/blanc-800x800.png",
|
|
},
|
|
["fields"] = fields,
|
|
["description"] = "",
|
|
["author"] = {
|
|
["name"] = "ESX Framework",
|
|
["icon_url"] = "https://cdn.discordapp.com/emojis/939245183621558362.webp?size=128&quality=lossless",
|
|
},
|
|
},
|
|
}
|
|
PerformHttpRequest(
|
|
webHook,
|
|
function()
|
|
return
|
|
end,
|
|
"POST",
|
|
json.encode({
|
|
username = "Logs",
|
|
embeds = embedData,
|
|
}),
|
|
{
|
|
["Content-Type"] = "application/json",
|
|
}
|
|
)
|
|
end
|
|
|
|
---@return nil
|
|
function ESX.RefreshJobs()
|
|
Core.JobsLoaded = false
|
|
|
|
local Jobs = {}
|
|
local jobs = MySQL.query.await("SELECT * FROM jobs")
|
|
|
|
for _, v in ipairs(jobs) do
|
|
Jobs[v.name] = v
|
|
Jobs[v.name].grades = {}
|
|
end
|
|
|
|
local jobGrades = MySQL.query.await("SELECT * FROM job_grades")
|
|
|
|
for _, v in ipairs(jobGrades) do
|
|
if Jobs[v.job_name] then
|
|
Jobs[v.job_name].grades[tostring(v.grade)] = v
|
|
else
|
|
print(('[^3WARNING^7] Ignoring job grades for ^5"%s"^0 due to missing job'):format(v.job_name))
|
|
end
|
|
end
|
|
|
|
for _, v in pairs(Jobs) do
|
|
if ESX.Table.SizeOf(v.grades) == 0 then
|
|
Jobs[v.name] = nil
|
|
print(('[^3WARNING^7] Ignoring job ^5"%s"^0 due to no job grades found'):format(v.name))
|
|
end
|
|
end
|
|
|
|
if not Jobs then
|
|
-- Fallback data, if no jobs exist
|
|
ESX.Jobs["unemployed"] = { name = "unemployed", label = "Unemployed", whitelisted = false, grades = { ["0"] = { grade = 0, name = "unemployed", label = "Unemployed", salary = 200, skin_male = {}, skin_female = {} } } }
|
|
else
|
|
ESX.Jobs = Jobs
|
|
end
|
|
|
|
TriggerEvent("esx:jobsRefreshed")
|
|
Core.JobsLoaded = true
|
|
end
|
|
|
|
---@param item string
|
|
---@param cb function
|
|
---@return nil
|
|
function ESX.RegisterUsableItem(item, cb)
|
|
Core.UsableItemsCallbacks[item] = cb
|
|
end
|
|
|
|
---@param source number
|
|
---@param item string
|
|
---@param ... any
|
|
---@return nil
|
|
function ESX.UseItem(source, item, ...)
|
|
if ESX.Items[item] then
|
|
local itemCallback = Core.UsableItemsCallbacks[item]
|
|
|
|
if itemCallback then
|
|
local success, result = pcall(itemCallback, source, item, ...)
|
|
|
|
if not success then
|
|
return result and print(result) or
|
|
print(('[^3WARNING^7] An error occured when using item ^5"%s"^7! This was not caused by ESX.'):format(
|
|
item))
|
|
end
|
|
end
|
|
else
|
|
print(('[^3WARNING^7] Item ^5"%s"^7 was used but does not exist!'):format(item))
|
|
end
|
|
end
|
|
|
|
---@param index string
|
|
---@param overrides table
|
|
---@return nil
|
|
function ESX.RegisterPlayerFunctionOverrides(index, overrides)
|
|
Core.PlayerFunctionOverrides[index] = overrides
|
|
end
|
|
|
|
---@param index string
|
|
---@return nil
|
|
function ESX.SetPlayerFunctionOverride(index)
|
|
if not index or not Core.PlayerFunctionOverrides[index] then
|
|
return print("[^3WARNING^7] No valid index provided.")
|
|
end
|
|
|
|
Config.PlayerFunctionOverride = index
|
|
end
|
|
|
|
---@param item string
|
|
---@return string?
|
|
---@diagnostic disable-next-line: duplicate-set-field
|
|
function ESX.GetItemLabel(item)
|
|
if ESX.Items[item] then
|
|
return ESX.Items[item].label
|
|
else
|
|
print(("[^3WARNING^7] Attemting to get invalid Item -> ^5%s^7"):format(item))
|
|
end
|
|
end
|
|
|
|
---@return table
|
|
function ESX.GetJobs()
|
|
while not Core.JobsLoaded do
|
|
Citizen.Wait(200)
|
|
end
|
|
|
|
return ESX.Jobs
|
|
end
|
|
|
|
---@return table
|
|
function ESX.GetItems()
|
|
return ESX.Items
|
|
end
|
|
|
|
---@return table
|
|
function ESX.GetUsableItems()
|
|
local Usables = {}
|
|
for k in pairs(Core.UsableItemsCallbacks) do
|
|
Usables[k] = true
|
|
end
|
|
return Usables
|
|
end
|
|
|
|
if not Config.CustomInventory then
|
|
---@param itemType string
|
|
---@param name string
|
|
---@param count integer
|
|
---@param label string
|
|
---@param playerId number
|
|
---@param components? string | table
|
|
---@param tintIndex? integer
|
|
---@param coords? table | vector3
|
|
---@return nil
|
|
function ESX.CreatePickup(itemType, name, count, label, playerId, components, tintIndex, coords)
|
|
local pickupId = (Core.PickupId == 65635 and 0 or Core.PickupId + 1)
|
|
local xPlayer = ESX.Players[playerId]
|
|
coords = ((type(coords) == "vector3" or type(coords) == "vector4") and coords.xyz or xPlayer.getCoords(true))
|
|
|
|
Core.Pickups[pickupId] = { type = itemType, name = name, count = count, label = label, coords = coords }
|
|
|
|
if itemType == "item_weapon" then
|
|
Core.Pickups[pickupId].components = components
|
|
Core.Pickups[pickupId].tintIndex = tintIndex
|
|
end
|
|
|
|
TriggerClientEvent("esx:createPickup", -1, pickupId, label, coords, itemType, name, components, tintIndex)
|
|
Core.PickupId = pickupId
|
|
end
|
|
|
|
local function refreshPlayerInventories()
|
|
local xPlayers = ESX.GetExtendedPlayers()
|
|
for i = 1, #xPlayers do
|
|
local xPlayer = xPlayers[i]
|
|
local minimalInv = xPlayer.getInventory(true)
|
|
|
|
for itemName, _ in pairs(minimalInv) do
|
|
if not ESX.Items[itemName] then
|
|
xPlayer.setInventoryItem(itemName, 0)
|
|
minimalInv[itemName] = nil
|
|
end
|
|
end
|
|
|
|
xPlayer.inventory = {}
|
|
local playerInvIndex = 1
|
|
for itemName, itemData in pairs(ESX.Items) do
|
|
xPlayer.inventory[playerInvIndex] = {
|
|
name = itemName,
|
|
count = minimalInv[itemName] or 0,
|
|
label = itemData.label,
|
|
weight = itemData.weight,
|
|
usable = Core.UsableItemsCallbacks[itemName] ~= nil,
|
|
rare = itemData.rare,
|
|
canRemove = itemData.canRemove,
|
|
}
|
|
playerInvIndex += 1
|
|
end
|
|
|
|
TriggerClientEvent("esx:setInventory", xPlayer.source, xPlayer.inventory)
|
|
end
|
|
end
|
|
|
|
---@return number newItemCount
|
|
function ESX.RefreshItems()
|
|
ESX.Items = {}
|
|
|
|
local items = MySQL.query.await("SELECT * FROM items")
|
|
local itemCount = #items
|
|
for i = 1, itemCount do
|
|
local item = items[i]
|
|
ESX.Items[item.name] = { label = item.label, weight = item.weight, rare = item.rare, canRemove = item
|
|
.can_remove }
|
|
end
|
|
refreshPlayerInventories()
|
|
|
|
return itemCount
|
|
end
|
|
|
|
---@param items { name: string, label: string, weight?: number, rare?: boolean, canRemove?: boolean }[]
|
|
function ESX.AddItems(items)
|
|
local toInsert = {}
|
|
local toInsertIndex = 1
|
|
|
|
for i = 1, #items do
|
|
local item = items[i]
|
|
local name = item.name
|
|
local label = item.label
|
|
local weight = item.weight or 1
|
|
local rare = item.rare or false
|
|
local canRemove = item.canRemove ~= false
|
|
|
|
if type(name) ~= "string" then
|
|
print(("^1[AddItems]^0 Invalid item name: %s"):format(name))
|
|
goto continue
|
|
end
|
|
|
|
if ESX.Items[name] then
|
|
goto continue
|
|
end
|
|
|
|
if type(label) ~= "string" then
|
|
print(("^1[AddItems]^0 Invalid label for item '%s'"):format(name))
|
|
goto continue
|
|
end
|
|
|
|
if type(weight) ~= "number" then
|
|
print(("^1[AddItems]^0 Invalid weight for item '%s'"):format(name))
|
|
goto continue
|
|
end
|
|
|
|
if type(rare) ~= "boolean" then
|
|
print(("^1[AddItems]^0 Invalid rare flag for item '%s'"):format(name))
|
|
goto continue
|
|
end
|
|
|
|
if type(canRemove) ~= "boolean" then
|
|
print(("^1[AddItems]^0 Invalid canRemove flag for item '%s'"):format(name))
|
|
goto continue
|
|
end
|
|
|
|
toInsert[toInsertIndex] = {
|
|
name = name,
|
|
label = label,
|
|
weight = weight,
|
|
rare = rare,
|
|
canRemove = canRemove,
|
|
}
|
|
toInsertIndex += 1
|
|
|
|
::continue::
|
|
end
|
|
|
|
if #toInsert > 0 then
|
|
MySQL.prepare.await(
|
|
"INSERT IGNORE INTO `items` (`name`, `label`, `weight`, `rare`, `can_remove`) VALUES (?, ?, ?, ?, ?)",
|
|
toInsert)
|
|
|
|
for i = 1, #toInsert do
|
|
local row = toInsert[i]
|
|
ESX.Items[row.name] = {
|
|
label = row.label,
|
|
weight = row.weight,
|
|
rare = row.rare,
|
|
canRemove = row.canRemove,
|
|
}
|
|
end
|
|
|
|
refreshPlayerInventories()
|
|
end
|
|
end
|
|
end
|
|
|
|
---@param job string
|
|
---@param grade string
|
|
---@return boolean
|
|
function ESX.DoesJobExist(job, grade)
|
|
while not Core.JobsLoaded do
|
|
Citizen.Wait(200)
|
|
end
|
|
|
|
return (ESX.Jobs[job] and ESX.Jobs[job].grades[tostring(grade)] ~= nil) or false
|
|
end
|
|
|
|
---@param playerSrc number
|
|
---@return boolean
|
|
function Core.IsPlayerAdmin(playerSrc)
|
|
if type(playerSrc) ~= "number" then
|
|
return false
|
|
end
|
|
|
|
if IsPlayerAceAllowed(playerSrc --[[@as string]], "command") or GetConvar("sv_lan", "") == "true" then
|
|
return true
|
|
end
|
|
|
|
local xPlayer = ESX.GetPlayerFromId(playerSrc)
|
|
return xPlayer and Config.AdminGroups[xPlayer.getGroup()] or false
|
|
end
|
|
|
|
-- Generates a unique 9-digit SSN in dashed format (XXX-XX-XXXX).
|
|
---@param skipUniqueCheck boolean?
|
|
---@return string
|
|
function Core.generateSSN(skipUniqueCheck)
|
|
local reservedSSNs = {
|
|
["078-05-1120"] = true,
|
|
["219-09-9999"] = true,
|
|
["123-45-6789"] = true
|
|
}
|
|
|
|
while true do
|
|
-- Generate the first part (area number)
|
|
local area = math.random(1, 899)
|
|
|
|
-- 666 is never assigned
|
|
if area == 666 then
|
|
goto continue
|
|
end
|
|
|
|
-- Generate the second part (group number)
|
|
local group = math.random(1, 99)
|
|
|
|
-- Generate the last part (serial number)
|
|
local serial = math.random(1, 9999)
|
|
|
|
-- Skip reserved SSN range (987-65-4320..4329)
|
|
if area == 987 and group == 65 and serial >= 4320 and serial <= 4329 then
|
|
goto continue
|
|
end
|
|
|
|
local candidate = string.format("%03d-%02d-%04d", area, group, serial)
|
|
|
|
if reservedSSNs[candidate] then
|
|
goto continue
|
|
end
|
|
|
|
if skipUniqueCheck then
|
|
return candidate
|
|
end
|
|
|
|
local exists = MySQL.scalar.await("SELECT 1 FROM `users` WHERE `ssn` = ? LIMIT 1", { candidate })
|
|
|
|
if not exists then
|
|
return candidate
|
|
end
|
|
|
|
::continue::
|
|
end
|
|
end
|
|
|
|
---@param owner string
|
|
---@param plate string
|
|
---@param coords vector4
|
|
---@return CExtendedVehicle?
|
|
function ESX.CreateExtendedVehicle(owner, plate, coords)
|
|
return Core.vehicleClass.new(owner, plate, coords)
|
|
end
|
|
|
|
---@param plate string
|
|
---@return CExtendedVehicle?
|
|
function ESX.GetExtendedVehicleFromPlate(plate)
|
|
return Core.vehicleClass.getFromPlate(plate)
|
|
end
|