diff --git a/config/server.lua b/config/server.lua index f06cb26e5..b8bfd3041 100644 --- a/config/server.lua +++ b/config/server.lua @@ -1,5 +1,5 @@ return { - updateInterval = 5, -- how often to update player data in minutes + updateInterval = 5, -- how often to update player thirst and hunger in minutes money = { ---@alias MoneyType 'cash' | 'bank' | 'crypto' diff --git a/server/loops.lua b/server/loops.lua index c7f3818e0..b73e852fa 100644 --- a/server/loops.lua +++ b/server/loops.lua @@ -10,7 +10,6 @@ local function removeHungerAndThirst(src, player) player.Functions.SetMetaData('hunger', math.max(0, newHunger)) TriggerClientEvent('hud:client:UpdateNeeds', src, newHunger, newThirst) - player.Functions.Save() end CreateThread(function() @@ -58,4 +57,4 @@ CreateThread(function() pay(player) end end -end) +end) \ No newline at end of file diff --git a/server/player.lua b/server/player.lua index 65f52e961..f9012eb12 100644 --- a/server/player.lua +++ b/server/player.lua @@ -54,8 +54,10 @@ exports('Login', Login) ---@return Player? player if found in storage function GetOfflinePlayer(citizenid) if not citizenid then return end + local playerData = storage.fetchPlayerEntity(citizenid) if not playerData then return end + return CheckPlayerData(nil, playerData) end @@ -95,7 +97,9 @@ function SetPlayerPrimaryJob(citizenid, jobName) assert(job.grades[grade] ~= nil, string.format('job %s does not have grade %s', jobName, grade)) player.PlayerData.job = toPlayerJob(jobName, job, grade) - player.Functions.Save() + storage.setPlayerPrimaryJob(citizenid, player.PlayerData.job) + player.Functions.SetPlayerData('job', player.PlayerData.job) + if not player.Offline then player.Functions.UpdatePlayerData() TriggerEvent('QBCore:Server:OnJobUpdate', player.PlayerData.source, player.PlayerData.job) @@ -112,6 +116,7 @@ exports('SetPlayerPrimaryJob', SetPlayerPrimaryJob) function AddPlayerToJob(citizenid, jobName, grade) -- unemployed job is the default, so players cannot be added to it if jobName == 'unemployed' then return end + local job = GetJob(jobName) assert(job ~= nil, 'job not found: ' .. jobName) assert(job.grades[grade] ~= nil, string.format('job %s does not have grade %s', jobName, grade)) @@ -119,15 +124,18 @@ function AddPlayerToJob(citizenid, jobName, grade) local player = GetPlayerByCitizenId(citizenid) or GetOfflinePlayer(citizenid) assert(player ~= nil, string.format('player not found with citizenid %s', citizenid)) if player.PlayerData.jobs[jobName] == grade then return end + assert(qbx.table.size(player.PlayerData.jobs) < maxJobsPerPlayer or player.PlayerData.jobs[jobName], 'player already has maximum amount of jobs allowed') + player.PlayerData.jobs[jobName] = grade + player.Functions.SetPlayerData('jobs', player.PlayerData.jobs) storage.addPlayerToJob(citizenid, jobName, grade) + if not player.Offline then - player.PlayerData.jobs[jobName] = grade - player.Functions.SetPlayerData('jobs', player.PlayerData.jobs) TriggerEvent('qbx_core:server:onGroupUpdate', player.PlayerData.source, jobName, grade) TriggerClientEvent('qbx_core:client:onGroupUpdate', player.PlayerData.source, jobName, grade) end + if player.PlayerData.job.name == jobName then SetPlayerPrimaryJob(citizenid, jobName) end @@ -141,6 +149,7 @@ exports('AddPlayerToJob', AddPlayerToJob) function RemovePlayerFromJob(citizenid, jobName) -- Unemployed is the default job, so players cannot be removed from it. if jobName == 'unemployed' then return end + local player = GetPlayerByCitizenId(citizenid) or GetOfflinePlayer(citizenid) assert(player ~= nil, string.format('player not found with citizenid %s', citizenid)) @@ -148,15 +157,13 @@ function RemovePlayerFromJob(citizenid, jobName) storage.removePlayerFromJob(citizenid, jobName) player.PlayerData.jobs[jobName] = nil + player.Functions.SetPlayerData('jobs', player.PlayerData.jobs) + if player.PlayerData.job.name == jobName then - local job = GetJob('unemployed') - assert(job ~= nil, 'cannot find unemployed job. Does it exist in shared/jobs.lua?') - player.PlayerData.job = toPlayerJob('unemployed', job, 0) - player.Functions.Save() + SetPlayerPrimaryJob(citizenid, 'unemployed') end if not player.Offline then - player.Functions.SetPlayerData('jobs', player.PlayerData.jobs) TriggerEvent('qbx_core:server:onGroupUpdate', player.PlayerData.source, jobName) TriggerClientEvent('qbx_core:client:onGroupUpdate', player.PlayerData.source, jobName) end @@ -188,7 +195,8 @@ local function setPlayerPrimaryGang(citizenid, gangName) } } - player.Functions.Save() + storage.setPlayerPrimaryGang(citizenid, player.PlayerData.gang) + player.Functions.SetPlayerData('gang', player.PlayerData.gang) if not player.Offline then player.Functions.UpdatePlayerData() @@ -218,13 +226,15 @@ function AddPlayerToGang(citizenid, gangName, grade) assert(qbx.table.size(player.PlayerData.gangs) < maxGangsPerPlayer or player.PlayerData.gangs[gangName], 'player already has maximum amount of gangs allowed') + player.PlayerData.gangs[gangName] = grade + player.Functions.SetPlayerData('gangs', player.PlayerData.gangs) storage.addPlayerToGang(citizenid, gangName, grade) + if not player.Offline then - player.PlayerData.gangs[gangName] = grade - player.Functions.SetPlayerData('gangs', player.PlayerData.gangs) TriggerEvent('qbx_core:server:onGroupUpdate', player.PlayerData.source, gangName, grade) TriggerClientEvent('qbx_core:client:onGroupUpdate', player.PlayerData.source, gangName, grade) end + if player.PlayerData.gang.name == gangName then setPlayerPrimaryGang(citizenid, gangName) end @@ -245,23 +255,13 @@ local function removePlayerFromGang(citizenid, gangName) storage.removePlayerFromGang(citizenid, gangName) player.PlayerData.gangs[gangName] = nil + player.Functions.SetPlayerData('gangs', player.PlayerData.gangs) + if player.PlayerData.gang.name == gangName then - local gang = GetGang('none') - assert(gang ~= nil, 'cannot find none gang. Does it exist in shared/gangs.lua?') - player.PlayerData.gang = { - name = gangName, - label = gang.label, - isboss = false, - grade = { - name = gang.grades[0].name, - level = 0 - } - } - player.Functions.Save() + setPlayerPrimaryGang(citizenid, 'none') end if not player.Offline then - player.Functions.SetPlayerData('gangs', player.PlayerData.gangs) TriggerEvent('qbx_core:server:onGroupUpdate', player.PlayerData.source, gangName) TriggerClientEvent('qbx_core:client:onGroupUpdate', player.PlayerData.source, gangName) end @@ -274,7 +274,7 @@ exports('RemovePlayerFromGang', removePlayerFromGang) ---@return Player player function CheckPlayerData(source, playerData) playerData = playerData or {} - local playerState = Player(source)?.state + local playerState = source and Player(source)?.state local Offline = true if source then playerData.source = source @@ -287,6 +287,7 @@ function CheckPlayerData(source, playerData) playerData.cid = playerData.charinfo?.cid or playerData.cid or 1 playerData.money = playerData.money or {} playerData.optin = playerData.optin or true + for moneytype, startamount in pairs(config.money.moneyTypes) do playerData.money[moneytype] = playerData.money[moneytype] or startamount end @@ -302,6 +303,7 @@ function CheckPlayerData(source, playerData) playerData.charinfo.phone = playerData.charinfo.phone or GenerateUniqueIdentifier('PhoneNumber') playerData.charinfo.account = playerData.charinfo.account or GenerateUniqueIdentifier('AccountNumber') playerData.charinfo.cid = playerData.charinfo.cid or playerData.cid + -- Metadata playerData.metadata = playerData.metadata or {} playerData.metadata.health = playerData.metadata.health or 200 @@ -356,10 +358,11 @@ function CheckPlayerData(source, playerData) SerialNumber = GenerateUniqueIdentifier('SerialNumber'), InstalledApps = {}, } - local jobs, gangs = storage.fetchPlayerGroups(playerData.citizenid) + local jobs, gangs = storage.fetchPlayerGroups(playerData.citizenid) local job = GetJob(playerData.job?.name) or GetJob('unemployed') assert(job ~= nil, 'Unemployed job not found. Does it exist in shared/jobs.lua?') + local jobGrade = GetJob(playerData.job?.name) and playerData.job.grade.level or 0 playerData.job = { @@ -374,14 +377,18 @@ function CheckPlayerData(source, playerData) level = jobGrade, } } - if QBX.Shared.ForceJobDefaultDutyAtLogin and (job.defaultDuty ~= nil) then + + if QBX.Shared.ForceJobDefaultDutyAtLogin and job.defaultDuty ~= nil then playerData.job.onduty = job.defaultDuty end playerData.jobs = jobs or {} + local gang = GetGang(playerData.gang?.name) or GetGang('none') assert(gang ~= nil, 'none gang not found. Does it exist in shared/gangs.lua?') + local gangGrade = GetGang(playerData.gang?.name) and playerData.gang.grade.level or 0 + playerData.gang = { name = playerData.gang?.name or 'none', label = gang.label, @@ -391,17 +398,20 @@ function CheckPlayerData(source, playerData) level = gangGrade } } + playerData.gangs = gangs or {} playerData.position = playerData.position or defaultSpawn playerData.items = GetResourceState('qb-inventory') ~= 'missing' and exports['qb-inventory']:LoadInventory(playerData.source, playerData.citizenid) or {} + return CreatePlayer(playerData --[[@as PlayerData]], Offline) end ----On player logout +---Trigger a player logout ---@param source Source function Logout(source) local player = GetPlayer(source) if not player then return end + local playerState = Player(source)?.state player.PlayerData.metadata.hunger = playerState?.hunger or player.PlayerData.metadata.hunger player.PlayerData.metadata.thirst = playerState?.thirst or player.PlayerData.metadata.thirst @@ -473,15 +483,19 @@ function CreatePlayer(playerData, Offline) lib.print.error(('cannot set job. Job %s does not exist'):format(jobName)) return false end + if not job.grades[grade] then lib.print.error(('cannot set job. Job %s does not have grade %s'):format(jobName, grade)) return false end + if setJobReplaces then RemovePlayerFromJob(self.PlayerData.citizenid, self.PlayerData.job.name) end + AddPlayerToJob(self.PlayerData.citizenid, jobName, grade) SetPlayerPrimaryJob(self.PlayerData.citizenid, jobName) + return true end @@ -497,15 +511,19 @@ function CreatePlayer(playerData, Offline) lib.print.error(('cannot set gang. Gang %s does not exist'):format(gangName)) return false end + if not gang.grades[grade] then lib.print.error(('cannot set gang. Gang %s does not have grade %s'):format(gangName, grade)) return false end + if setGangReplaces then removePlayerFromGang(self.PlayerData.citizenid, self.PlayerData.gang.name) end + AddPlayerToGang(self.PlayerData.citizenid, gangName, grade) setPlayerPrimaryGang(self.PlayerData.citizenid, gangName) + return true end @@ -514,14 +532,17 @@ function CreatePlayer(playerData, Offline) self.PlayerData.job.onduty = not not onDuty -- Make sure the value is a boolean if nil is sent TriggerEvent('QBCore:Server:SetDuty', self.PlayerData.source, self.PlayerData.job.onduty) TriggerClientEvent('QBCore:Client:SetDuty', self.PlayerData.source, self.PlayerData.job.onduty) + storage.setPlayerPrimaryJob(self.PlayerData.citizenid, self.PlayerData.job) self.Functions.UpdatePlayerData() end ---@param key string ---@param val any function self.Functions.SetPlayerData(key, val) - if not key or type(key) ~= 'string' then return end + if type(key) ~= 'string' then return end + self.PlayerData[key] = val + self.Functions.Save() self.Functions.UpdatePlayerData() end @@ -529,6 +550,7 @@ function CreatePlayer(playerData, Offline) ---@param val any function self.Functions.SetMetaData(meta, val) if not meta or type(meta) ~= 'string' then return end + if (meta == 'hunger' or meta == 'thirst' or meta == 'stress') and self.PlayerData.source then val = lib.math.clamp(val, 0, 100) Player(self.PlayerData.source).state:set(meta, val, true) @@ -536,26 +558,27 @@ function CreatePlayer(playerData, Offline) local oldVal = self.PlayerData.metadata[meta] self.PlayerData.metadata[meta] = val + storage.setPlayerMetadata(self.PlayerData.citizenid, self.PlayerData.metadata) self.Functions.UpdatePlayerData() - if meta == 'inlaststand' or meta == 'isdead' then - self.Functions.Save() - end TriggerClientEvent('qbx_core:client:onSetMetaData', self.PlayerData.source, meta, oldVal, val) - TriggerEvent('qbx_core:server:onSetMetaData', meta, oldVal, val, self.PlayerData.source) + TriggerEvent('qbx_core:server:onSetMetaData', meta, oldVal, val, self.PlayerData.source) end ---@param meta string ---@return any function self.Functions.GetMetaData(meta) if not meta or type(meta) ~= 'string' then return end + return self.PlayerData.metadata[meta] end ---@param amount number function self.Functions.AddJobReputation(amount) if not amount then return end + amount = tonumber(amount) --[[@as number]] self.PlayerData.metadata.jobrep[self.PlayerData.job.name] = self.PlayerData.metadata.jobrep[self.PlayerData.job.name] + amount + storage.setPlayerMetadata(self.PlayerData.citizenid, self.PlayerData.metadata) self.Functions.UpdatePlayerData() end @@ -566,9 +589,10 @@ function CreatePlayer(playerData, Offline) function self.Functions.AddMoney(moneytype, amount, reason) reason = reason or 'unknown' amount = qbx.math.round(tonumber(amount) --[[@as number]]) - if amount < 0 then return false end - if not self.PlayerData.money[moneytype] then return false end - self.PlayerData.money[moneytype] = self.PlayerData.money[moneytype] + amount + if not self.PlayerData.money[moneytype] or amount < 0 then return false end + + self.PlayerData.money[moneytype] += amount + storage.setPlayerMoney(self.PlayerData.citizenid, self.PlayerData.money) if not self.Offline then self.Functions.UpdatePlayerData() @@ -596,8 +620,8 @@ function CreatePlayer(playerData, Offline) function self.Functions.RemoveMoney(moneytype, amount, reason) reason = reason or 'unknown' amount = qbx.math.round(tonumber(amount) --[[@as number]]) - if amount < 0 then return false end - if not self.PlayerData.money[moneytype] then return false end + if not self.PlayerData.money[moneytype] or amount < 0 then return false end + for _, mtype in pairs(config.money.dontAllowMinus) do if mtype == moneytype then if (self.PlayerData.money[moneytype] - amount) < 0 then @@ -605,10 +629,13 @@ function CreatePlayer(playerData, Offline) end end end - self.PlayerData.money[moneytype] = self.PlayerData.money[moneytype] - amount + + self.PlayerData.money[moneytype] -= amount + storage.setPlayerMoney(self.PlayerData.citizenid, self.PlayerData.money) if not self.Offline then self.Functions.UpdatePlayerData() + local tags = amount > 100000 and config.logging.role or nil logger.log({ source = 'qbx_core', @@ -618,10 +645,13 @@ function CreatePlayer(playerData, Offline) tags = tags, message = ('** %s (citizenid: %s | id: %s)** $%s (%s) removed, new %s balance: $%s reason: %s'):format(GetPlayerName(self.PlayerData.source), self.PlayerData.citizenid, self.PlayerData.source, amount, moneytype, moneytype, self.PlayerData.money[moneytype], reason), }) + 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 @@ -636,13 +666,15 @@ function CreatePlayer(playerData, Offline) function self.Functions.SetMoney(moneytype, amount, reason) reason = reason or 'unknown' amount = qbx.math.round(tonumber(amount) --[[@as number]]) - if amount < 0 then return false end - if not self.PlayerData.money[moneytype] then return false end + if not self.PlayerData.money[moneytype] or amount < 0 then return false end + local difference = amount - self.PlayerData.money[moneytype] self.PlayerData.money[moneytype] = amount + storage.setPlayerMoney(self.PlayerData.citizenid, self.PlayerData.money) if not self.Offline then self.Functions.UpdatePlayerData() + logger.log({ source = 'qbx_core', webhook = config.logging.webhook['playermoney'], @@ -650,6 +682,7 @@ function CreatePlayer(playerData, Offline) color = 'green', message = ('**%s (citizenid: %s | id: %s)** $%s (%s) set, new %s balance: $%s reason: %s'):format(GetPlayerName(self.PlayerData.source), self.PlayerData.citizenid, self.PlayerData.source, amount, moneytype, moneytype, self.PlayerData.money[moneytype], 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) @@ -662,12 +695,14 @@ function CreatePlayer(playerData, Offline) ---@return boolean | number amount or false if moneytype does not exist function self.Functions.GetMoney(moneytype) if not moneytype then return false end + return self.PlayerData.money[moneytype] end ---@param cardNumber number function self.Functions.SetCreditCard(cardNumber) self.PlayerData.charinfo.card = cardNumber + storage.setPlayerCharInfo(self.PlayerData.citizenid, self.PlayerData.charinfo) self.Functions.UpdatePlayerData() end @@ -682,15 +717,18 @@ function CreatePlayer(playerData, Offline) ---@deprecated call exports.qbx_core:Logout(source) function self.Functions.Logout() if self.Offline then return end -- Unsupported for Offline Players + Logout(self.PlayerData.source) end AddEventHandler('qbx_core:server:onJobUpdate', function(jobName, job) if self.PlayerData.job.name ~= jobName then return end + if not job then - self.Functions.setJob('unemployed', 0) + self.Functions.SetJob('unemployed', 0) return end + self.PlayerData.job.label = job.label self.PlayerData.job.type = job.type or 'none' local jobGrade = job.grades[self.PlayerData.job.grade.level] @@ -707,6 +745,8 @@ function CreatePlayer(playerData, Offline) } end + storage.setPlayerPrimaryJob(self.PlayerData.citizenid, self.PlayerData.job) + if not self.Offline then self.Functions.UpdatePlayerData() TriggerEvent('QBCore:Server:OnJobUpdate', self.PlayerData.source, self.PlayerData.job) @@ -716,6 +756,7 @@ function CreatePlayer(playerData, Offline) AddEventHandler('qbx_core:server:onGangUpdate', function(gangName, gang) if self.PlayerData.gang.name ~= gangName then return end + if not gang then self.PlayerData.gang = { name = 'none', @@ -739,6 +780,9 @@ function CreatePlayer(playerData, Offline) self.PlayerData.gang.isboss = false end end + + storage.setPlayerPrimaryGang(self.PlayerData.citizenid, self.PlayerData.gang) + if not self.Offline then self.Functions.UpdatePlayerData() TriggerEvent('QBCore:Server:OnGangUpdate', self.PlayerData.source, self.PlayerData.gang) @@ -773,6 +817,7 @@ function Save(source) local coords = GetEntityCoords(ped) pcoords = vec4(coords.x, coords.y, coords.z, GetEntityHeading(ped)) end + if not playerData then lib.print.error('QBX.PLAYER.SAVE - PLAYERDATA IS EMPTY!') return @@ -793,7 +838,9 @@ function Save(source) position = pcoords, }) end) + if GetResourceState('qb-inventory') ~= 'missing' then exports['qb-inventory']:SaveInventory(source) end + lib.print.verbose(('%s PLAYER SAVED!'):format(playerData.name)) end @@ -812,7 +859,9 @@ function SaveOffline(playerData) position = playerData.position.xyz }) end) + if GetResourceState('qb-inventory') ~= 'missing' then exports['qb-inventory']:SaveInventory(playerData, true) end + lib.print.verbose(('%s OFFLINE PLAYER SAVED!'):format(playerData.name)) end @@ -885,7 +934,8 @@ function GenerateUniqueIdentifier(type) uniqueId = table.valueFunction() isUnique = storage.fetchIsUnique(type, uniqueId) until isUnique + return uniqueId end -exports('GenerateUniqueIdentifier', GenerateUniqueIdentifier) +exports('GenerateUniqueIdentifier', GenerateUniqueIdentifier) \ No newline at end of file diff --git a/server/storage/main.lua b/server/storage/main.lua index ff774cf9d..71a11e26b 100644 --- a/server/storage/main.lua +++ b/server/storage/main.lua @@ -15,6 +15,11 @@ local players = require 'server.storage.players' ---@field fetchPlayerGroups fun(citizenid: string): table, table jobs, gangs ---@field removePlayerFromJob fun(citizenid: string, group: string) ---@field removePlayerFromGang fun(citizenid: string, group: string) +---@field setPlayerMetadata fun(citizenid: string, metadata: PlayerMetadata): boolean success +---@field setPlayerPrimaryJob fun(citizenid: string, job: PlayerJob): boolean success +---@field setPlayerPrimaryGang fun(citizenid: string, gang: PlayerGang): boolean success +---@field setPlayerMoney fun(citizenid: string, money: Money): boolean success +---@field setPlayerCharInfo fun(citizenid: string, charinfo: PlayerCharInfo): boolean success ---@type StorageFunctions return players \ No newline at end of file diff --git a/server/storage/players.lua b/server/storage/players.lua index 226b49873..4fba0ed87 100644 --- a/server/storage/players.lua +++ b/server/storage/players.lua @@ -230,8 +230,8 @@ end ---@param tableName string ---@return boolean local function doesTableExist(tableName) - local tbl = MySQL.single.await(('SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_NAME = \'%s\' AND TABLE_SCHEMA in (SELECT DATABASE())'):format(tableName)) - return tbl['COUNT(*)'] > 0 + local result = MySQL.single.await(('SELECT COUNT(*) as count FROM information_schema.TABLES WHERE TABLE_NAME = \'%s\' AND TABLE_SCHEMA in (SELECT DATABASE())'):format(tableName)) + return result.count > 0 end ---deletes character data using the characterDataTables object in the config file @@ -340,6 +340,46 @@ local function removePlayerFromGang(citizenid, group) removeFromGroup(citizenid, GroupType.GANG, group) end +---@param citizenid string +---@param job PlayerJob +---@return boolean success if operation is successful. +local function setPlayerPrimaryJob(citizenid, job) + local affectedRows = MySQL.update.await('UPDATE players SET job = ? WHERE citizenid = ?', {json.encode(job), citizenid}) + return affectedRows > 0 +end + +---@param citizenid string +---@param gang PlayerGang +---@return boolean success if operation is successful. +local function setPlayerPrimaryGang(citizenid, gang) + local affectedRows = MySQL.update.await('UPDATE players SET gang = ? WHERE citizenid = ?', {json.encode(gang), citizenid}) + return affectedRows > 0 +end + +---@param citizenid string +---@param metadata PlayerMetadata +---@return boolean success if operation is successful. +local function setPlayerMetadata(citizenid, metadata) + local affectedRows = MySQL.update.await('UPDATE players SET metadata = ? WHERE citizenid = ?', {json.encode(metadata), citizenid}) + return affectedRows > 0 +end + +---@param citizenid string +---@param money Money +---@return boolean success if operation is successful. +local function setPlayerMoney(citizenid, money) + local affectedRows = MySQL.update.await('UPDATE players SET money = ? WHERE citizenid = ?', {json.encode(money), citizenid}) + return affectedRows > 0 +end + +---@param citizenid string +---@param charinfo PlayerCharInfo +---@return boolean success if operation is successful. +local function setPlayerCharInfo(citizenid, charinfo) + local affectedRows = MySQL.update.await('UPDATE players SET charinfo = ? WHERE citizenid = ?', {json.encode(charinfo), citizenid}) + return affectedRows > 0 +end + ---Copies player's primary job/gang to the player_groups table. Works for online/offline players. ---Idempotent RegisterCommand('convertjobs', function(source) @@ -381,4 +421,9 @@ return { fetchPlayerGroups = fetchPlayerGroups, removePlayerFromJob = removePlayerFromJob, removePlayerFromGang = removePlayerFromGang, -} + setPlayerMetadata = setPlayerMetadata, + setPlayerPrimaryJob = setPlayerPrimaryJob, + setPlayerPrimaryGang = setPlayerPrimaryGang, + setPlayerMoney = setPlayerMoney, + setPlayerCharInfo = setPlayerCharInfo +} \ No newline at end of file