mirror of
https://github.com/qbcore-fivem/qb-core.git
synced 2026-08-28 22:01:33 +00:00
704 lines
29 KiB
Lua
704 lines
29 KiB
Lua
QBCore.Players = {}
|
|
QBCore.Player = {}
|
|
QBCore.PlayersByCitizenId = {}
|
|
local resourceName = GetCurrentResourceName()
|
|
|
|
-- ─────────────────────────── Player class ───────────────────────────────────
|
|
|
|
local varargMethods = {
|
|
'GetPlayerData', 'UpdateClient', 'SetJob', 'SetGang', 'Notify', 'HasItem',
|
|
'SetJobDuty', 'SetPlayerData', 'SetMetaData', 'GetMetaData',
|
|
'AddRep', 'RemoveRep', 'GetRep', 'AddMoney', 'RemoveMoney', 'SetMoney', 'GetMoney',
|
|
'AddMethod', 'AddField',
|
|
}
|
|
local noargMethods = { 'GetName', 'Save', 'Logout' }
|
|
|
|
local function isCallable(value)
|
|
if type(value) == 'function' then return true end
|
|
if type(value) == 'table' and type(rawget(value, '__cfx_functionReference')) == 'string' then return true end
|
|
local mt = getmetatable(value)
|
|
return mt and type(mt.__call) == 'function'
|
|
end
|
|
|
|
local function buildMethodTable(player)
|
|
local t = {}
|
|
for _, name in ipairs(varargMethods) do
|
|
local fn = player[name]
|
|
t[name] = function(...) return fn(player, ...) end
|
|
end
|
|
for _, name in ipairs(noargMethods) do
|
|
local fn = player[name]
|
|
t[name] = function() return fn(player) end
|
|
end
|
|
return t
|
|
end
|
|
|
|
local Player = {}
|
|
Player.__index = Player
|
|
|
|
function Player.new(PlayerData, Offline)
|
|
local self = setmetatable({}, Player)
|
|
self.PlayerData = PlayerData
|
|
self.Offline = Offline or false
|
|
self.Functions = buildMethodTable(self)
|
|
self.Functions.UpdatePlayerData = self.Functions.UpdateClient
|
|
return self
|
|
end
|
|
|
|
-- ─────────────────────────── instance methods ───────────────────────────────
|
|
|
|
function Player:GetPlayerData()
|
|
return self.PlayerData
|
|
end
|
|
|
|
function Player:UpdateClient(key, val)
|
|
if self.Offline then return end
|
|
if key then
|
|
TriggerEvent('QBCore:Server:OnPlayerUpdated', self.PlayerData.source, key, val)
|
|
TriggerClientEvent('QBCore:Client:OnPlayerUpdated', self.PlayerData.source, key, val)
|
|
else
|
|
TriggerEvent('QBCore:Player:SetPlayerData', self.PlayerData)
|
|
TriggerEvent('QBCore:Server:OnPlayerUpdated', self.PlayerData.source, 'all', self.PlayerData)
|
|
TriggerClientEvent('QBCore:Client:OnPlayerUpdated', self.PlayerData.source, 'all', self.PlayerData)
|
|
end
|
|
end
|
|
|
|
function Player:SetJob(job, grade)
|
|
job = job:lower()
|
|
grade = grade or '0'
|
|
if not QBCore.Shared.Jobs[job] then return false end
|
|
self.PlayerData.job = {
|
|
name = job,
|
|
label = QBCore.Shared.Jobs[job].label,
|
|
onduty = QBCore.Shared.Jobs[job].defaultDuty,
|
|
type = QBCore.Shared.Jobs[job].type or 'none',
|
|
grade = {
|
|
name = 'No Grades',
|
|
level = 0,
|
|
payment = 30,
|
|
isboss = false,
|
|
}
|
|
}
|
|
local gradeKey = tostring(grade)
|
|
local jobGradeInfo = QBCore.Shared.Jobs[job].grades[gradeKey]
|
|
if jobGradeInfo then
|
|
self.PlayerData.job.grade.name = jobGradeInfo.name
|
|
self.PlayerData.job.grade.level = tonumber(gradeKey)
|
|
self.PlayerData.job.grade.payment = jobGradeInfo.payment
|
|
self.PlayerData.job.grade.isboss = jobGradeInfo.isboss or false
|
|
self.PlayerData.job.isboss = jobGradeInfo.isboss or false
|
|
end
|
|
if not self.Offline then
|
|
self:UpdateClient('job', self.PlayerData.job)
|
|
end
|
|
return true
|
|
end
|
|
|
|
function Player:SetGang(gang, grade)
|
|
gang = gang:lower()
|
|
grade = grade or '0'
|
|
if not QBCore.Shared.Gangs[gang] then return false end
|
|
self.PlayerData.gang = {
|
|
name = gang,
|
|
label = QBCore.Shared.Gangs[gang].label,
|
|
grade = {
|
|
name = 'No Grades',
|
|
level = 0,
|
|
isboss = false,
|
|
}
|
|
}
|
|
local gradeKey = tostring(grade)
|
|
local gangGradeInfo = QBCore.Shared.Gangs[gang].grades[gradeKey]
|
|
if gangGradeInfo then
|
|
self.PlayerData.gang.grade.name = gangGradeInfo.name
|
|
self.PlayerData.gang.grade.level = tonumber(gradeKey)
|
|
self.PlayerData.gang.grade.isboss = gangGradeInfo.isboss or false
|
|
self.PlayerData.gang.isboss = gangGradeInfo.isboss or false
|
|
end
|
|
if not self.Offline then
|
|
self:UpdateClient('gang', self.PlayerData.gang)
|
|
end
|
|
return true
|
|
end
|
|
|
|
function Player:Notify(text, notifyType, length)
|
|
TriggerClientEvent('QBCore:Notify', self.PlayerData.source, text, notifyType, length)
|
|
end
|
|
|
|
function Player:HasItem(items, amount)
|
|
return QBCore.Functions.HasItem(self.PlayerData.source, items, amount)
|
|
end
|
|
|
|
function Player:GetName()
|
|
local charinfo = self.PlayerData.charinfo
|
|
return charinfo.firstname .. ' ' .. charinfo.lastname
|
|
end
|
|
|
|
function Player:SetJobDuty(onDuty)
|
|
self.PlayerData.job.onduty = not not onDuty
|
|
if not self.Offline then
|
|
self:UpdateClient('job', self.PlayerData.job)
|
|
end
|
|
end
|
|
|
|
function Player:SetPlayerData(key, val)
|
|
if not key or type(key) ~= 'string' then return end
|
|
self.PlayerData[key] = val
|
|
self:UpdateClient(key, val)
|
|
end
|
|
|
|
function Player:SetMetaData(meta, val)
|
|
if not meta or type(meta) ~= 'string' then return end
|
|
if meta == 'hunger' or meta == 'thirst' then
|
|
val = math.min(100, math.max(0, val))
|
|
end
|
|
self.PlayerData.metadata[meta] = val
|
|
self:UpdateClient('metadata', self.PlayerData.metadata)
|
|
end
|
|
|
|
function Player:GetMetaData(meta)
|
|
if not meta or type(meta) ~= 'string' then return end
|
|
return self.PlayerData.metadata[meta]
|
|
end
|
|
|
|
function Player:AddRep(rep, amount)
|
|
if not rep or not amount then return end
|
|
local addAmount = tonumber(amount)
|
|
local currentRep = self.PlayerData.metadata['rep'][rep] or 0
|
|
self.PlayerData.metadata['rep'][rep] = currentRep + addAmount
|
|
self:UpdateClient('metadata', self.PlayerData.metadata)
|
|
end
|
|
|
|
function Player:RemoveRep(rep, amount)
|
|
if not rep or not amount then return end
|
|
local removeAmount = tonumber(amount)
|
|
local currentRep = self.PlayerData.metadata['rep'][rep] or 0
|
|
self.PlayerData.metadata['rep'][rep] = math.max(0, currentRep - removeAmount)
|
|
self:UpdateClient('metadata', self.PlayerData.metadata)
|
|
end
|
|
|
|
function Player:GetRep(rep)
|
|
if not rep then return end
|
|
return self.PlayerData.metadata['rep'][rep] or 0
|
|
end
|
|
|
|
function Player:AddMoney(moneytype, amount, reason)
|
|
reason = reason or 'unknown'
|
|
moneytype = moneytype:lower()
|
|
amount = tonumber(amount)
|
|
if not amount or amount < 0 then return end
|
|
if not self.PlayerData.money[moneytype] then return false end
|
|
self.PlayerData.money[moneytype] = self.PlayerData.money[moneytype] + amount
|
|
if not self.Offline then
|
|
self:UpdateClient('money', self.PlayerData.money)
|
|
local logExtra = amount > 100000
|
|
TriggerEvent('qb-log:server:CreateLog', 'playermoney', 'AddMoney', 'lightgreen',
|
|
'**' .. self.PlayerData.name ..
|
|
' (citizenid: ' .. self.PlayerData.citizenid ..
|
|
' | id: ' .. self.PlayerData.source .. ')** $' .. amount ..
|
|
' (' .. moneytype .. ') added, new ' .. moneytype ..
|
|
' balance: ' .. self.PlayerData.money[moneytype] ..
|
|
' reason: ' .. reason, logExtra)
|
|
TriggerClientEvent('hud:client:OnMoneyChange', self.PlayerData.source, moneytype, amount, false)
|
|
TriggerClientEvent('QBCore:Client:OnMoneyChange', self.PlayerData.source, moneytype, amount, 'add', reason)
|
|
TriggerEvent('QBCore:Server:OnMoneyChange', self.PlayerData.source, moneytype, amount, 'add', reason)
|
|
end
|
|
return true
|
|
end
|
|
|
|
function Player:RemoveMoney(moneytype, amount, reason)
|
|
reason = reason or 'unknown'
|
|
moneytype = moneytype:lower()
|
|
amount = tonumber(amount)
|
|
if not amount or amount < 0 then return end
|
|
if not self.PlayerData.money[moneytype] then return false end
|
|
for _, mtype in pairs(QBCore.Config.Money.DontAllowMinus) do
|
|
if mtype == moneytype and (self.PlayerData.money[moneytype] - amount) < 0 then
|
|
return false
|
|
end
|
|
end
|
|
if self.PlayerData.money[moneytype] - amount < QBCore.Config.Money.MinusLimit then return false end
|
|
self.PlayerData.money[moneytype] = self.PlayerData.money[moneytype] - amount
|
|
if not self.Offline then
|
|
self:UpdateClient('money', self.PlayerData.money)
|
|
local logExtra = amount > 100000
|
|
TriggerEvent('qb-log:server:CreateLog', 'playermoney', 'RemoveMoney', 'red',
|
|
'**' .. self.PlayerData.name ..
|
|
' (citizenid: ' .. self.PlayerData.citizenid ..
|
|
' | id: ' .. self.PlayerData.source .. ')** $' .. amount ..
|
|
' (' .. moneytype .. ') removed, new ' .. moneytype ..
|
|
' balance: ' .. self.PlayerData.money[moneytype] ..
|
|
' reason: ' .. reason, logExtra)
|
|
TriggerClientEvent('hud:client:OnMoneyChange', self.PlayerData.source, moneytype, amount, true)
|
|
if moneytype == 'bank' then
|
|
TriggerClientEvent('qb-phone:client:RemoveBankMoney', self.PlayerData.source, amount)
|
|
end
|
|
TriggerClientEvent('QBCore:Client:OnMoneyChange', self.PlayerData.source, moneytype, amount, 'remove', reason)
|
|
TriggerEvent('QBCore:Server:OnMoneyChange', self.PlayerData.source, moneytype, amount, 'remove', reason)
|
|
end
|
|
return true
|
|
end
|
|
|
|
function Player:SetMoney(moneytype, amount, reason)
|
|
reason = reason or 'unknown'
|
|
moneytype = moneytype:lower()
|
|
amount = tonumber(amount)
|
|
if not amount or amount < 0 then return false end
|
|
if not self.PlayerData.money[moneytype] then return false end
|
|
local difference = amount - self.PlayerData.money[moneytype]
|
|
self.PlayerData.money[moneytype] = amount
|
|
if not self.Offline then
|
|
self:UpdateClient('money', self.PlayerData.money)
|
|
TriggerEvent('qb-log:server:CreateLog', 'playermoney', 'SetMoney', 'green',
|
|
'**' .. self.PlayerData.name ..
|
|
' (citizenid: ' .. self.PlayerData.citizenid ..
|
|
' | id: ' .. self.PlayerData.source .. ')** $' .. amount ..
|
|
' (' .. moneytype .. ') set, new ' .. moneytype ..
|
|
' balance: ' .. self.PlayerData.money[moneytype] ..
|
|
' reason: ' .. reason)
|
|
TriggerClientEvent('hud:client:OnMoneyChange', self.PlayerData.source, moneytype, math.abs(difference), difference < 0)
|
|
TriggerClientEvent('QBCore:Client:OnMoneyChange', self.PlayerData.source, moneytype, amount, 'set', reason)
|
|
TriggerEvent('QBCore:Server:OnMoneyChange', self.PlayerData.source, moneytype, amount, 'set', reason)
|
|
end
|
|
return true
|
|
end
|
|
|
|
function Player:GetMoney(moneytype)
|
|
if not moneytype then return false end
|
|
return self.PlayerData.money[moneytype:lower()]
|
|
end
|
|
|
|
function Player:Save()
|
|
if self.Offline then
|
|
QBCore.Player.SaveOffline(self.PlayerData)
|
|
else
|
|
QBCore.Player.Save(self.PlayerData.source)
|
|
end
|
|
end
|
|
|
|
function Player:Logout()
|
|
if self.Offline then return end
|
|
QBCore.Player.Logout(self.PlayerData.source)
|
|
end
|
|
|
|
function Player:AddMethod(methodName, handler)
|
|
if type(methodName) ~= 'string' or not isCallable(handler) then return false end
|
|
self[methodName] = handler
|
|
self.Functions[methodName] = handler
|
|
return true
|
|
end
|
|
|
|
function Player:AddField(fieldName, data)
|
|
if type(fieldName) ~= 'string' or isCallable(data) then return false end
|
|
self[fieldName] = data
|
|
return true
|
|
end
|
|
|
|
-- ─────────────────────────── module-private helpers ─────────────────────────
|
|
|
|
local function decodePlayerFields(PlayerData)
|
|
PlayerData.money = json.decode(PlayerData.money)
|
|
PlayerData.job = json.decode(PlayerData.job)
|
|
PlayerData.gang = json.decode(PlayerData.gang)
|
|
PlayerData.position = json.decode(PlayerData.position)
|
|
PlayerData.metadata = json.decode(PlayerData.metadata)
|
|
PlayerData.charinfo = json.decode(PlayerData.charinfo)
|
|
end
|
|
|
|
local function applyDefaults(playerData, defaults)
|
|
for key, value in pairs(defaults) do
|
|
if type(value) == 'function' then
|
|
playerData[key] = playerData[key] or value()
|
|
elseif type(value) == 'table' then
|
|
playerData[key] = playerData[key] or {}
|
|
applyDefaults(playerData[key], value)
|
|
else
|
|
playerData[key] = playerData[key] or value
|
|
end
|
|
end
|
|
end
|
|
|
|
-- ─────────────────────────── login / logout ─────────────────────────────────
|
|
|
|
function QBCore.Player.Login(source, citizenid, newData)
|
|
if source and source ~= '' then
|
|
if citizenid then
|
|
local license = QBCore.Functions.GetIdentifier(source, 'license')
|
|
local PlayerData = MySQL.prepare.await('SELECT * FROM players where citizenid = ?', { citizenid })
|
|
if PlayerData and license == PlayerData.license then
|
|
decodePlayerFields(PlayerData)
|
|
QBCore.Player.CheckPlayerData(source, PlayerData)
|
|
else
|
|
DropPlayer(source, Lang:t('info.exploit_dropped'))
|
|
TriggerEvent('qb-log:server:CreateLog', 'anticheat', 'Anti-Cheat', 'white', GetPlayerName(source) .. ' Has Been Dropped For Character Joining Exploit', false)
|
|
return false
|
|
end
|
|
else
|
|
QBCore.Player.CheckPlayerData(source, newData)
|
|
end
|
|
return true
|
|
else
|
|
QBCore.ShowError(resourceName, 'ERROR QBCORE.PLAYER.LOGIN - NO SOURCE GIVEN!')
|
|
return false
|
|
end
|
|
end
|
|
|
|
function QBCore.Player.Logout(source)
|
|
local player = QBCore.Players[source]
|
|
if not player then return end
|
|
player.Functions.Save()
|
|
TriggerClientEvent('QBCore:Client:OnPlayerUnload', source)
|
|
TriggerEvent('QBCore:Server:OnPlayerUnload', source)
|
|
QBCore.PlayersByCitizenId[player.PlayerData.citizenid] = nil
|
|
QBCore.Players[source] = nil
|
|
end
|
|
|
|
-- ─────────────────────────── offline player lookups ─────────────────────────
|
|
|
|
function QBCore.Player.GetOfflinePlayer(citizenid)
|
|
if not citizenid then return nil end
|
|
local PlayerData = MySQL.prepare.await('SELECT * FROM players where citizenid = ?', { citizenid })
|
|
if PlayerData then
|
|
decodePlayerFields(PlayerData)
|
|
return QBCore.Player.CheckPlayerData(nil, PlayerData)
|
|
end
|
|
return nil
|
|
end
|
|
|
|
function QBCore.Player.GetPlayerByLicense(license)
|
|
if not license then return nil end
|
|
local source = QBCore.Functions.GetSource(license)
|
|
if source > 0 then
|
|
return QBCore.Players[source]
|
|
else
|
|
return QBCore.Player.GetOfflinePlayerByLicense(license)
|
|
end
|
|
end
|
|
|
|
function QBCore.Player.GetOfflinePlayerByLicense(license)
|
|
if not license then return nil end
|
|
local PlayerData = MySQL.prepare.await('SELECT * FROM players where license = ?', { license })
|
|
if PlayerData then
|
|
decodePlayerFields(PlayerData)
|
|
return QBCore.Player.CheckPlayerData(nil, PlayerData)
|
|
end
|
|
return nil
|
|
end
|
|
|
|
-- ─────────────────────────── data validation / construction ─────────────────
|
|
|
|
function QBCore.Player.CheckPlayerData(source, PlayerData)
|
|
PlayerData = PlayerData or {}
|
|
local Offline = not source
|
|
|
|
if source then
|
|
PlayerData.source = source
|
|
PlayerData.license = PlayerData.license or QBCore.Functions.GetIdentifier(source, 'license')
|
|
PlayerData.name = GetPlayerName(source)
|
|
end
|
|
|
|
local validatedJob = false
|
|
if PlayerData.job and PlayerData.job.name ~= nil and PlayerData.job.grade and PlayerData.job.grade.level ~= nil then
|
|
local jobInfo = QBCore.Shared.Jobs[PlayerData.job.name]
|
|
if jobInfo then
|
|
local jobGradeInfo = jobInfo.grades[tostring(PlayerData.job.grade.level)]
|
|
if jobGradeInfo then
|
|
PlayerData.job.label = jobInfo.label
|
|
PlayerData.job.grade.name = jobGradeInfo.name
|
|
PlayerData.job.grade.payment = jobGradeInfo.payment
|
|
PlayerData.job.grade.isboss = jobGradeInfo.isboss or false
|
|
PlayerData.job.isboss = jobGradeInfo.isboss or false
|
|
validatedJob = true
|
|
end
|
|
end
|
|
end
|
|
|
|
if not validatedJob then PlayerData.job = nil end
|
|
|
|
local validatedGang = false
|
|
if PlayerData.gang and PlayerData.gang.name ~= nil and PlayerData.gang.grade and PlayerData.gang.grade.level ~= nil then
|
|
local gangInfo = QBCore.Shared.Gangs[PlayerData.gang.name]
|
|
if gangInfo then
|
|
local gangGradeInfo = gangInfo.grades[tostring(PlayerData.gang.grade.level)]
|
|
if gangGradeInfo then
|
|
PlayerData.gang.label = gangInfo.label
|
|
PlayerData.gang.grade.name = gangGradeInfo.name
|
|
PlayerData.gang.grade.payment = gangGradeInfo.payment
|
|
PlayerData.gang.grade.isboss = gangGradeInfo.isboss or false
|
|
PlayerData.gang.isboss = gangGradeInfo.isboss or false
|
|
validatedGang = true
|
|
end
|
|
end
|
|
end
|
|
|
|
if not validatedGang then PlayerData.gang = nil end
|
|
|
|
applyDefaults(PlayerData, QBCore.Config.Player.PlayerDefaults)
|
|
|
|
if PlayerData.job and QBCore.Shared.ForceJobDefaultDutyAtLogin then
|
|
local jobInfo = QBCore.Shared.Jobs[PlayerData.job.name]
|
|
if jobInfo then
|
|
PlayerData.job.onduty = jobInfo.defaultDuty
|
|
end
|
|
end
|
|
|
|
if not Offline and GetResourceState('qb-inventory') ~= 'missing' then
|
|
PlayerData.items = exports['qb-inventory']:LoadInventory(PlayerData.source, PlayerData.citizenid)
|
|
end
|
|
|
|
return QBCore.Player.CreatePlayer(PlayerData, Offline)
|
|
end
|
|
|
|
function QBCore.Player.CreatePlayer(PlayerData, Offline)
|
|
local player = Player.new(PlayerData, Offline)
|
|
if Offline then return player end
|
|
QBCore.Players[PlayerData.source] = player
|
|
QBCore.PlayersByCitizenId[PlayerData.citizenid] = player
|
|
QBCore.Player.Save(PlayerData.source)
|
|
TriggerEvent('QBCore:Server:PlayerLoaded', player)
|
|
player:UpdateClient()
|
|
return player
|
|
end
|
|
|
|
-- ─────────────────────────── runtime extension API ──────────────────────────
|
|
|
|
local function forEachPlayer(ids, fn)
|
|
local idType = type(ids)
|
|
if idType == 'number' then
|
|
if ids == -1 then
|
|
for _, v in pairs(QBCore.Players) do fn(v) end
|
|
elseif QBCore.Players[ids] then
|
|
fn(QBCore.Players[ids])
|
|
end
|
|
elseif idType == 'table' and table.type(ids) == 'array' then
|
|
for i = 1, #ids do forEachPlayer(ids[i], fn) end
|
|
end
|
|
end
|
|
|
|
function QBCore.Functions.AddPlayerMethod(ids, methodName, handler)
|
|
forEachPlayer(ids, function(v) v:AddMethod(methodName, handler) end)
|
|
end
|
|
|
|
function QBCore.Functions.AddPlayerField(ids, fieldName, data)
|
|
forEachPlayer(ids, function(v) v:AddField(fieldName, data) end)
|
|
end
|
|
|
|
-- ─────────────────────────── save / persistence ─────────────────────────────
|
|
|
|
function QBCore.Player.Save(source)
|
|
local ped = GetPlayerPed(source)
|
|
local pcoords = GetEntityCoords(ped)
|
|
local PlayerData = QBCore.Players[source] and QBCore.Players[source].PlayerData
|
|
if PlayerData then
|
|
MySQL.insert('INSERT INTO players (citizenid, cid, license, name, money, charinfo, job, gang, position, metadata) VALUES (:citizenid, :cid, :license, :name, :money, :charinfo, :job, :gang, :position, :metadata) ON DUPLICATE KEY UPDATE cid = :cid, name = :name, money = :money, charinfo = :charinfo, job = :job, gang = :gang, position = :position, metadata = :metadata', {
|
|
citizenid = PlayerData.citizenid,
|
|
cid = tonumber(PlayerData.cid),
|
|
license = PlayerData.license,
|
|
name = PlayerData.name,
|
|
money = json.encode(PlayerData.money),
|
|
charinfo = json.encode(PlayerData.charinfo),
|
|
job = json.encode(PlayerData.job),
|
|
gang = json.encode(PlayerData.gang),
|
|
position = json.encode(pcoords),
|
|
metadata = json.encode(PlayerData.metadata)
|
|
})
|
|
if GetResourceState('qb-inventory') ~= 'missing' then exports['qb-inventory']:SaveInventory(source) end
|
|
QBCore.ShowSuccess(resourceName, PlayerData.name .. ' PLAYER SAVED!')
|
|
else
|
|
QBCore.ShowError(resourceName, 'ERROR QBCORE.PLAYER.SAVE - PLAYERDATA IS EMPTY!')
|
|
end
|
|
end
|
|
|
|
function QBCore.Player.SaveOffline(PlayerData)
|
|
if PlayerData then
|
|
MySQL.insert('INSERT INTO players (citizenid, cid, license, name, money, charinfo, job, gang, position, metadata) VALUES (:citizenid, :cid, :license, :name, :money, :charinfo, :job, :gang, :position, :metadata) ON DUPLICATE KEY UPDATE cid = :cid, name = :name, money = :money, charinfo = :charinfo, job = :job, gang = :gang, position = :position, metadata = :metadata', {
|
|
citizenid = PlayerData.citizenid,
|
|
cid = tonumber(PlayerData.cid),
|
|
license = PlayerData.license,
|
|
name = PlayerData.name,
|
|
money = json.encode(PlayerData.money),
|
|
charinfo = json.encode(PlayerData.charinfo),
|
|
job = json.encode(PlayerData.job),
|
|
gang = json.encode(PlayerData.gang),
|
|
position = json.encode(PlayerData.position),
|
|
metadata = json.encode(PlayerData.metadata)
|
|
})
|
|
if GetResourceState('qb-inventory') ~= 'missing' then exports['qb-inventory']:SaveInventory(PlayerData, true) end
|
|
QBCore.ShowSuccess(resourceName, PlayerData.name .. ' OFFLINE PLAYER SAVED!')
|
|
else
|
|
QBCore.ShowError(resourceName, 'ERROR QBCORE.PLAYER.SAVEOFFLINE - PLAYERDATA IS EMPTY!')
|
|
end
|
|
end
|
|
|
|
-- ─────────────────────────── character deletion ─────────────────────────────
|
|
|
|
local playertables = {
|
|
{ table = 'players' },
|
|
{ table = 'apartments' },
|
|
{ table = 'bank_accounts' },
|
|
{ table = 'crypto_transactions' },
|
|
{ table = 'phone_invoices' },
|
|
{ table = 'phone_messages' },
|
|
{ table = 'playerskins' },
|
|
{ table = 'player_contacts' },
|
|
{ table = 'player_houses' },
|
|
{ table = 'player_mails' },
|
|
{ table = 'player_outfits' },
|
|
{ table = 'player_vehicles' },
|
|
}
|
|
|
|
function QBCore.Player.DeleteCharacter(source, citizenid)
|
|
local license = QBCore.Functions.GetIdentifier(source, 'license')
|
|
local result = MySQL.scalar.await('SELECT license FROM players where citizenid = ?', { citizenid })
|
|
if license == result then
|
|
local query = 'DELETE FROM %s WHERE citizenid = ?'
|
|
local queries = {}
|
|
for i = 1, #playertables do
|
|
queries[i] = { query = query:format(playertables[i].table), values = { citizenid } }
|
|
end
|
|
MySQL.transaction(queries, function(result2)
|
|
if result2 then
|
|
TriggerEvent('qb-log:server:CreateLog', 'joinleave', 'Character Deleted', 'red',
|
|
'**' .. GetPlayerName(source) .. '** ' .. license .. ' deleted **' .. citizenid .. '**..')
|
|
end
|
|
end)
|
|
else
|
|
DropPlayer(source, Lang:t('info.exploit_dropped'))
|
|
TriggerEvent('qb-log:server:CreateLog', 'anticheat', 'Anti-Cheat', 'white', GetPlayerName(source) .. ' Has Been Dropped For Character Deletion Exploit', true)
|
|
end
|
|
end
|
|
|
|
function QBCore.Player.ForceDeleteCharacter(citizenid)
|
|
local result = MySQL.scalar.await('SELECT license FROM players where citizenid = ?', { citizenid })
|
|
if not result then return end
|
|
local query = 'DELETE FROM %s WHERE citizenid = ?'
|
|
local queries = {}
|
|
local existing = QBCore.Functions.GetPlayerByCitizenId(citizenid)
|
|
if existing then
|
|
local src = existing.PlayerData.source
|
|
-- clear refs to prevent playerDropped from saving
|
|
QBCore.Players[src] = nil
|
|
QBCore.PlayersByCitizenId[citizenid] = nil
|
|
DropPlayer(src, 'An admin deleted the character which you are currently using')
|
|
end
|
|
for i = 1, #playertables do
|
|
queries[i] = { query = query:format(playertables[i].table), values = { citizenid } }
|
|
end
|
|
MySQL.transaction(queries, function(result2)
|
|
if result2 then
|
|
TriggerEvent('qb-log:server:CreateLog', 'joinleave', 'Character Force Deleted', 'red',
|
|
'Character **' .. citizenid .. '** got deleted')
|
|
end
|
|
end)
|
|
end
|
|
|
|
-- ─────────────────────────── unique ID generators ───────────────────────────
|
|
|
|
local function createUniqueId(generator, query, retries)
|
|
retries = (retries or 0) + 1
|
|
if retries > 100 then
|
|
QBCore.ShowError(resourceName, 'createUniqueId: exceeded 100 retries')
|
|
return nil
|
|
end
|
|
local value = generator()
|
|
local result = MySQL.prepare.await(query, { value })
|
|
if result == 0 then return value end
|
|
return createUniqueId(generator, query, retries)
|
|
end
|
|
|
|
function QBCore.Player.CreateCitizenId()
|
|
return createUniqueId(
|
|
function() return tostring(QBCore.Shared.RandomStr(3) .. QBCore.Shared.RandomInt(5)):upper() end,
|
|
'SELECT EXISTS(SELECT 1 FROM players WHERE citizenid = ?) AS uniqueCheck'
|
|
)
|
|
end
|
|
|
|
function QBCore.Functions.CreateAccountNumber()
|
|
return createUniqueId(
|
|
function() return 'US0' .. math.random(1, 9) .. 'QBCore' .. math.random(1111, 9999) .. math.random(1111, 9999) .. math.random(11, 99) end,
|
|
'SELECT EXISTS(SELECT 1 FROM players WHERE JSON_UNQUOTE(JSON_EXTRACT(charinfo, "$.account")) = ?) AS uniqueCheck'
|
|
)
|
|
end
|
|
|
|
function QBCore.Functions.CreatePhoneNumber()
|
|
return createUniqueId(
|
|
function() return math.random(100, 999) .. math.random(1000000, 9999999) end,
|
|
'SELECT EXISTS(SELECT 1 FROM players WHERE JSON_UNQUOTE(JSON_EXTRACT(charinfo, "$.phone")) = ?) AS uniqueCheck'
|
|
)
|
|
end
|
|
|
|
function QBCore.Player.CreateFingerId()
|
|
return createUniqueId(
|
|
function() return tostring(QBCore.Shared.RandomStr(2) .. QBCore.Shared.RandomInt(3) .. QBCore.Shared.RandomStr(1) .. QBCore.Shared.RandomInt(2) .. QBCore.Shared.RandomStr(3) .. QBCore.Shared.RandomInt(4)) end,
|
|
'SELECT EXISTS(SELECT 1 FROM players WHERE JSON_UNQUOTE(JSON_EXTRACT(metadata, "$.fingerprint")) = ?) AS uniqueCheck'
|
|
)
|
|
end
|
|
|
|
function QBCore.Player.CreateWalletId()
|
|
return createUniqueId(
|
|
function() return 'QB-' .. math.random(11111111, 99999999) end,
|
|
'SELECT EXISTS(SELECT 1 FROM players WHERE JSON_UNQUOTE(JSON_EXTRACT(metadata, "$.walletid")) = ?) AS uniqueCheck'
|
|
)
|
|
end
|
|
|
|
function QBCore.Player.CreateSerialNumber()
|
|
return createUniqueId(
|
|
function() return math.random(11111111, 99999999) end,
|
|
'SELECT EXISTS(SELECT 1 FROM players WHERE JSON_UNQUOTE(JSON_EXTRACT(metadata, "$.phonedata.SerialNumber")) = ?) AS uniqueCheck'
|
|
)
|
|
end
|
|
|
|
-- ─────────────────────────── export bridge ──────────────────────────────────
|
|
|
|
local function buildInterface(internalPlayer)
|
|
if not internalPlayer then return nil end
|
|
local iface, methods = {}, {}
|
|
|
|
for methodName, handler in pairs(internalPlayer.Functions) do
|
|
if isCallable(handler) then
|
|
iface[methodName] = handler
|
|
methods[methodName] = handler
|
|
end
|
|
end
|
|
|
|
for fieldName, data in pairs(internalPlayer) do
|
|
if fieldName ~= 'PlayerData' and fieldName ~= 'Functions' and fieldName ~= 'Offline' and not isCallable(data) then
|
|
iface[fieldName] = data
|
|
end
|
|
end
|
|
|
|
iface.PlayerData = internalPlayer.PlayerData
|
|
iface.Functions = methods
|
|
return iface
|
|
end
|
|
|
|
exports('GetPlayer', function(source)
|
|
return buildInterface(QBCore.Players[tonumber(source)])
|
|
end)
|
|
|
|
exports('GetPlayerByCitizenId', function(citizenid)
|
|
return buildInterface(QBCore.PlayersByCitizenId[citizenid])
|
|
end)
|
|
|
|
exports('GetOfflinePlayerByCitizenId', function(citizenid)
|
|
return buildInterface(QBCore.Player.GetOfflinePlayer(citizenid))
|
|
end)
|
|
|
|
exports('GetPlayerByLicense', function(license)
|
|
return buildInterface(QBCore.Player.GetPlayerByLicense(license))
|
|
end)
|
|
|
|
exports('GetOfflinePlayerByLicense', function(license)
|
|
return buildInterface(QBCore.Player.GetOfflinePlayerByLicense(license))
|
|
end)
|
|
|
|
exports('AddPlayerMethod', function(ids, methodName, handler)
|
|
QBCore.Functions.AddPlayerMethod(ids, methodName, handler)
|
|
end)
|
|
|
|
exports('AddPlayerField', function(ids, fieldName, data)
|
|
QBCore.Functions.AddPlayerField(ids, fieldName, data)
|
|
end)
|
|
|
|
PaycheckInterval()
|