diff --git a/config/server.lua b/config/server.lua index 21c903025..6429c9312 100644 --- a/config/server.lua +++ b/config/server.lua @@ -1,5 +1,6 @@ return { - updateInterval = 5, -- how often to update player data in minutes + updateInterval = 5, -- how often to update player thirst and hunger in minutes + dbUpdateInterval = 15, -- how often to update the database for player data in seconds money = { ---@alias MoneyType 'cash' | 'bank' | 'crypto' diff --git a/server/events.lua b/server/events.lua index 0acdf000d..c0b49d542 100644 --- a/server/events.lua +++ b/server/events.lua @@ -49,7 +49,7 @@ AddEventHandler('playerDropped', function(reason) color = 'red', message = ('**%s** (%s) left...\n **Reason:** %s'):format(GetPlayerName(src), player.PlayerData.license, reason), }) - player.Functions.Save() + Save(player.PlayerData.source) QBX.Player_Buckets[player.PlayerData.license] = nil QBX.Players[src] = nil end) diff --git a/server/functions.lua b/server/functions.lua index 5874397bb..1ff345266 100644 --- a/server/functions.lua +++ b/server/functions.lua @@ -358,7 +358,7 @@ function ToggleOptin(source) if not license or not IsPlayerAceAllowed(source --[[@as string]], 'admin') then return end local player = GetPlayer(source) player.PlayerData.optin = not player.PlayerData.optin - player.Functions.SetPlayerData('optin', player.PlayerData.optin) + SetPlayerData(player.PlayerData.source, 'optin', player.PlayerData.optin, nil, true) end exports('ToggleOptin', ToggleOptin) diff --git a/server/loops.lua b/server/loops.lua index eb64c5502..ae674831d 100644 --- a/server/loops.lua +++ b/server/loops.lua @@ -1,4 +1,5 @@ local config = require 'config.server' +local storage = require 'server.storage.main' local function removeHungerAndThirst(src, player) local playerState = Player(src).state @@ -8,8 +9,6 @@ local function removeHungerAndThirst(src, player) player.Functions.SetMetaData('thirst', math.max(0, newThirst)) player.Functions.SetMetaData('hunger', math.max(0, newHunger)) - - player.Functions.Save() end CreateThread(function() @@ -22,6 +21,14 @@ CreateThread(function() end end) +CreateThread(function() + local interval = 1000 * config.dbUpdateInterval + while true do + Wait(interval) + storage.sendPlayerDataUpdates() + end +end) + local function pay(player) local job = player.PlayerData.job local payment = GetJob(job.name).grades[job.grade.level].payment or job.payment diff --git a/server/player.lua b/server/player.lua index aff53fef3..85ddb9d38 100644 --- a/server/player.lua +++ b/server/player.lua @@ -135,16 +135,15 @@ exports('SetJob', SetJob) function SetJobDuty(identifier, onDuty) local player = type(identifier) == 'string' and (GetPlayerByCitizenId(identifier) or GetOfflinePlayer(identifier)) or GetPlayer(identifier) - if not player then return end - - player.PlayerData.job.onduty = not not onDuty - - if player.Offline then return end - - TriggerEvent('QBCore:Server:SetDuty', player.PlayerData.source, player.PlayerData.job.onduty) - TriggerClientEvent('QBCore:Client:SetDuty', player.PlayerData.source, player.PlayerData.job.onduty) + if not player then + lib.print.error(('SetJobDuty couldn\'t find player with identifier %s'):format(identifier)) + return + end - UpdatePlayerData(identifier) + SetPlayerData(identifier, {'job', 'onduty'}, not not onDuty, function() + TriggerEvent('QBCore:Server:SetDuty', player.PlayerData.source, player.PlayerData.job.onduty) + TriggerClientEvent('QBCore:Client:SetDuty', player.PlayerData.source, player.PlayerData.job.onduty) + end) end exports('SetJobDuty', SetJobDuty) @@ -202,14 +201,10 @@ function SetPlayerPrimaryJob(citizenid, jobName) player.PlayerData.job = toPlayerJob(jobName, job, grade) - if player.Offline then - SaveOffline(player.PlayerData) - else - Save(player.PlayerData.source) - UpdatePlayerData(player.PlayerData.source) + SetPlayerData(citizenid, 'job', player.PlayerData.job, function() TriggerEvent('QBCore:Server:OnJobUpdate', player.PlayerData.source, player.PlayerData.job) TriggerClientEvent('QBCore:Client:OnJobUpdate', player.PlayerData.source, player.PlayerData.job) - end + end) return true end @@ -270,12 +265,11 @@ function AddPlayerToJob(citizenid, jobName, grade) storage.addPlayerToJob(citizenid, jobName, grade) - if not player.Offline then - player.PlayerData.jobs[jobName] = grade - SetPlayerData(player.PlayerData.source, 'jobs', player.PlayerData.jobs) + player.PlayerData.jobs[jobName] = grade + SetPlayerData(citizenid, 'jobs', player.PlayerData.jobs, function() TriggerEvent('qbx_core:server:onGroupUpdate', player.PlayerData.source, jobName, grade) TriggerClientEvent('qbx_core:client:onGroupUpdate', player.PlayerData.source, jobName, grade) - end + end, true) if player.PlayerData.job.name == jobName then SetPlayerPrimaryJob(citizenid, jobName) @@ -317,19 +311,13 @@ function RemovePlayerFromJob(citizenid, jobName) 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) - if player.Offline then - SaveOffline(player.PlayerData) - else - Save(player.PlayerData.source) - end + SetPlayerData(citizenid, 'job', toPlayerJob('unemployed', job, 0)) end - if not player.Offline then - SetPlayerData(player.PlayerData.source, 'jobs', player.PlayerData.jobs) + SetPlayerData(citizenid, 'jobs', player.PlayerData.jobs, function() TriggerEvent('qbx_core:server:onGroupUpdate', player.PlayerData.source, jobName) TriggerClientEvent('qbx_core:client:onGroupUpdate', player.PlayerData.source, jobName) - end + end, true) return true end @@ -415,7 +403,7 @@ function SetPlayerPrimaryGang(citizenid, gangName) assert(gang.grades[grade] ~= nil, ('gang %s does not have grade %s'):format(gangName, grade)) - player.PlayerData.gang = { + SetPlayerData(citizenid, 'gang', { name = gangName, label = gang.label, isboss = gang.grades[grade].isboss, @@ -424,16 +412,10 @@ function SetPlayerPrimaryGang(citizenid, gangName) name = gang.grades[grade].name, level = grade } - } - - if player.Offline then - SaveOffline(player.PlayerData) - else - Save(player.PlayerData.source) - UpdatePlayerData(player.PlayerData.source) + }, function() TriggerEvent('QBCore:Server:OnGangUpdate', player.PlayerData.source, player.PlayerData.gang) TriggerClientEvent('QBCore:Client:OnGangUpdate', player.PlayerData.source, player.PlayerData.gang) - end + end) return true end @@ -493,12 +475,11 @@ function AddPlayerToGang(citizenid, gangName, grade) storage.addPlayerToGang(citizenid, gangName, grade) - if not player.Offline then - player.PlayerData.gangs[gangName] = grade - SetPlayerData(player.PlayerData.source, 'gangs', player.PlayerData.gangs) + player.PlayerData.gangs[gangName] = grade + SetPlayerData(citizenid, 'gangs', player.PlayerData.gangs, function() TriggerEvent('qbx_core:server:onGroupUpdate', player.PlayerData.source, gangName, grade) TriggerClientEvent('qbx_core:client:onGroupUpdate', player.PlayerData.source, gangName, grade) - end + end, true) if player.PlayerData.gang.name == gangName then SetPlayerPrimaryGang(citizenid, gangName) @@ -540,7 +521,8 @@ function RemovePlayerFromGang(citizenid, gangName) 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 = { + + SetPlayerData(citizenid, 'gang', { name = 'none', label = gang.label, isboss = false, @@ -549,19 +531,13 @@ function RemovePlayerFromGang(citizenid, gangName) name = gang.grades[0].name, level = 0 } - } - if player.Offline then - SaveOffline(player.PlayerData) - else - Save(player.PlayerData.source) - end + }) end - if not player.Offline then - SetPlayerData(player.PlayerData.source, 'gangs', player.PlayerData.gangs) + SetPlayerData(citizenid, 'gangs', player.PlayerData.gangs, function() TriggerEvent('qbx_core:server:onGroupUpdate', player.PlayerData.source, gangName) TriggerClientEvent('qbx_core:client:onGroupUpdate', player.PlayerData.source, gangName) - end + end, true) return true end @@ -740,14 +716,15 @@ function CreatePlayer(playerData, Offline) self.PlayerData = playerData self.Offline = Offline - ---@deprecated use UpdatePlayerData instead + ---@deprecated exports.qbx_core:SetPlayerData calls these events automatically function self.Functions.UpdatePlayerData() if self.Offline then lib.print.warn('UpdatePlayerData is unsupported for offline players') return end - UpdatePlayerData(self.PlayerData.source) + TriggerEvent('QBCore:Player:SetPlayerData', self.PlayerData) + TriggerClientEvent('QBCore:Player:SetPlayerData', self.PlayerData.source, self.PlayerData) end ---@deprecated use SetJob instead @@ -804,10 +781,7 @@ function CreatePlayer(playerData, Offline) amount = tonumber(amount) --[[@as number]] - self.PlayerData.metadata[self.PlayerData.job.name].reputation += amount - - ---@diagnostic disable-next-line: param-type-mismatch - UpdatePlayerData(self.Offline and self.PlayerData.citizenid or self.PlayerData.source) + SetPlayerData(self.PlayerData.source, 'metadata', {self.PlayerData.job.name, 'reputation'}, self.PlayerData.metadata[self.PlayerData.job.name].reputation + amount) end ---@param moneytype MoneyType @@ -911,16 +885,13 @@ function CreatePlayer(playerData, Offline) error('Player.Functions.SetInventory is unsupported for ox_inventory. Try ClearInventory, then add the desired items.') end - ---@deprecated use SetCharInfo instead + ---@deprecated use exports.qbx_core:SetCharInfo instead ---@param cardNumber number function self.Functions.SetCreditCard(cardNumber) - self.PlayerData.charinfo.card = cardNumber - - ---@diagnostic disable-next-line: param-type-mismatch - UpdatePlayerData(self.Offline and self.PlayerData.citizenid or self.PlayerData.source) + SetPlayerData(self.PlayerData.source, {'charinfo', 'card'}, cardNumber) end - ---@deprecated use Save or SaveOffline instead + ---@deprecated use exports.qbx_core:Save or exports.qbx_core:SaveOffline instead function self.Functions.Save() if self.Offline then SaveOffline(self.PlayerData) @@ -972,11 +943,10 @@ function CreatePlayer(playerData, Offline) end end - if not self.Offline then - UpdatePlayerData(self.PlayerData.source) + SetPlayerData(self.PlayerData.source, 'job', self.PlayerData.job, function() TriggerEvent('QBCore:Server:OnJobUpdate', self.PlayerData.source, self.PlayerData.job) TriggerClientEvent('QBCore:Client:OnJobUpdate', self.PlayerData.source, self.PlayerData.job) - end + end) end) AddEventHandler('qbx_core:server:onGangUpdate', function(gangName, gang) @@ -1012,11 +982,10 @@ function CreatePlayer(playerData, Offline) end end - if not self.Offline then - UpdatePlayerData(self.PlayerData.source) + SetPlayerData(self.PlayerData.source, 'gang', self.PlayerData.gang, function() TriggerEvent('QBCore:Server:OnGangUpdate', self.PlayerData.source, self.PlayerData.gang) TriggerClientEvent('QBCore:Client:OnGangUpdate', self.PlayerData.source, self.PlayerData.gang) - end + end) end) if not self.Offline then @@ -1026,7 +995,6 @@ function CreatePlayer(playerData, Offline) SetPedArmour(ped, self.PlayerData.metadata.armor) -- At this point we are safe to emit new instance to third party resource for load handling GlobalState.PlayerCount += 1 - UpdatePlayerData(self.PlayerData.source) Player(self.PlayerData.source).state:set('loadInventory', true, true) TriggerEvent('QBCore:Server:PlayerLoaded', self) end @@ -1062,6 +1030,8 @@ function Save(source) end CreateThread(function() + storage.forcePlayerDataUpdate(playerData.citizenid) + storage.upsertPlayerEntity({ playerEntity = playerData, position = pcoords, @@ -1093,33 +1063,63 @@ end exports('SaveOffline', SaveOffline) ---@param identifier Source | string ----@param key string +---@param key string | string[] ---@param value any -function SetPlayerData(identifier, key, value) - if type(key) ~= 'string' then return end +---@param cb? function A function that's called after the standard SetPlayerData events are triggered if the player is online +---@param cancelDbUpdate? boolean When true, makes sure the database doesn't get updated as a result of this change +function SetPlayerData(identifier, key, value, cb, cancelDbUpdate) + local hasSubKeys = type(key) == 'table' + if type(key) ~= 'string' and not hasSubKeys then return end local player = type(identifier) == 'string' and (GetPlayerByCitizenId(identifier) or GetOfflinePlayer(identifier)) or GetPlayer(identifier) - if not player then return end - - player.PlayerData[key] = value + if not player then + lib.print.error(('SetPlayerData couldn\'t find player with identifier %s'):format(identifier)) + return + end - UpdatePlayerData(identifier) -end + local oldValue = player.PlayerData[hasSubKeys and key[1] or key] + + if hasSubKeys then + local current = player.PlayerData[hasSubKeys and key[1] or key] + if #key > 2 then + -- We don't check the last one because otherwise we lose the table reference + for i = 2, #key - 1 do + local newCurrent = current[key[i]] + if newCurrent then + current = newCurrent + else + -- if an invalid key is specified , stop trying to update + lib.print.error(('key %s doesn\'t exist in table player.PlayerData.%s'):format(key[i], key[1])) + return + end + end + end -exports('SetPlayerData', SetPlayerData) + local lastIndex = #key + oldValue = current[key[lastIndex]] + current[key[lastIndex]] = value + else + player.PlayerData[key] = value + end ----@param identifier Source | string -function UpdatePlayerData(identifier) - local player = type(identifier) == 'string' and (GetPlayerByCitizenId(identifier) or GetOfflinePlayer(identifier)) or GetPlayer(identifier) + if not cancelDbUpdate then + storage.addPlayerDataUpdate(player.PlayerData.citizenid, key, value) + end - if not player or player.Offline then return end + if player.Offline then return end TriggerEvent('QBCore:Player:SetPlayerData', player.PlayerData) TriggerClientEvent('QBCore:Player:SetPlayerData', player.PlayerData.source, player.PlayerData) + TriggerEvent('qbx_core:server:setPlayerData', player.PlayerData.source, key, value, oldValue) + TriggerClientEvent('qbx_core:client:setPlayerData', player.PlayerData.source, key, value, oldValue) + + if not cb then return end + + cb() end -exports('UpdatePlayerData', UpdatePlayerData) +exports('SetPlayerData', SetPlayerData) ---@param identifier Source | string ---@param metadata string @@ -1131,54 +1131,25 @@ function SetMetadata(identifier, metadata, value) if not player then return end - local oldValue - - if metadata:match('%.') then - local metaTable, metaKey = metadata:match('([^%.]+)%.(.+)') - - if metaKey:match('%.') then - lib.print.error('cannot get nested metadata more than 1 level deep') - end - - oldValue = player.PlayerData.metadata[metaTable] - - player.PlayerData.metadata[metaTable][metaKey] = value - - metadata = metaTable - else - oldValue = player.PlayerData.metadata[metadata] - - player.PlayerData.metadata[metadata] = value + if metadata == 'hunger' or metadata == 'thirst' or metadata == 'stress' then + value = lib.math.clamp(value, 0, 100) end - UpdatePlayerData(identifier) - - if not player.Offline then + local oldValue = player.PlayerData.metadata[metadata] + SetPlayerData(identifier, {'metadata', metadata}, value, function() local playerState = Player(player.PlayerData.source).state TriggerClientEvent('qbx_core:client:onSetMetaData', player.PlayerData.source, metadata, oldValue, value) - TriggerEvent('qbx_core:server:onSetMetaData', metadata, oldValue, value, player.PlayerData.source) - - if (metadata == 'hunger' or metadata == 'thirst' or metadata == 'stress') then - value = lib.math.clamp(value, 0, 100) + TriggerEvent('qbx_core:server:onSetMetaData', metadata, oldValue, value, player.PlayerData.source) - if playerState[metadata] ~= value then - playerState:set(metadata, value, true) - end + if (metadata == 'hunger' or metadata == 'thirst' or metadata == 'stress') and playerState[metadata] ~= value then + playerState:set(metadata, value, true) end - if (metadata == 'dead' or metadata == 'inlaststand') then + if metadata == 'dead' or metadata == 'inlaststand' then playerState:set('canUseWeapons', not value, true) end - end - - if metadata == 'inlaststand' or metadata == 'isdead' then - if player.Offline then - SaveOffline(player.PlayerData) - else - Save(player.PlayerData.source) - end - end + end) end exports('SetMetadata', SetMetadata) @@ -1212,17 +1183,7 @@ exports('GetMetadata', GetMetadata) ---@param charInfo string ---@param value any function SetCharInfo(identifier, charInfo, value) - if type(charInfo) ~= 'string' then return end - - local player = type(identifier) == 'string' and (GetPlayerByCitizenId(identifier) or GetOfflinePlayer(identifier)) or GetPlayer(identifier) - - if not player then return end - - --local oldCharInfo = player.PlayerData.charinfo[charInfo] - - player.PlayerData.charinfo[charInfo] = value - - UpdatePlayerData(identifier) + SetPlayerData(identifier, 'charinfo', {charInfo}, value) end exports('SetCharInfo', SetCharInfo) @@ -1238,7 +1199,7 @@ local function emitMoneyEvents(source, playerMoney, moneyType, amount, actionTyp local isSet = actionType == 'set' local isRemove = actionType == 'remove' - TriggerClientEvent('hud:client:OnMoneyChange', source, moneyType, isSet and math.abs(difference) or amount, isSet and difference < 0 or isRemove, reason) + TriggerClientEvent('hud:client:OnMoneyChange', source, moneyType, isSet and difference and math.abs(difference) or amount, isSet and difference and difference < 0 or isRemove, reason) TriggerClientEvent('QBCore:Client:OnMoneyChange', source, moneyType, amount, actionType, reason) TriggerEvent('QBCore:Server:OnMoneyChange', source, moneyType, amount, actionType, reason) @@ -1274,11 +1235,7 @@ function AddMoney(identifier, moneyType, amount, reason) amount = amount }) then return false end - player.PlayerData.money[moneyType] += amount - - if not player.Offline then - UpdatePlayerData(identifier) - + SetPlayerData(identifier, {'money', moneyType}, player.PlayerData.money[moneyType] + amount, function() local tags = amount > 100000 and config.logging.role or nil local resource = GetInvokingResource() or cache.resource @@ -1293,7 +1250,7 @@ function AddMoney(identifier, moneyType, amount, reason) }) emitMoneyEvents(player.PlayerData.source, player.PlayerData.money, moneyType, amount, 'add', reason) - end + end) return true end @@ -1329,11 +1286,7 @@ function RemoveMoney(identifier, moneyType, amount, reason) end end - player.PlayerData.money[moneyType] -= amount - - if not player.Offline then - UpdatePlayerData(identifier) - + SetPlayerData(identifier, {'money', moneyType}, player.PlayerData.money[moneyType] - amount, function() local tags = amount > 100000 and config.logging.role or nil local resource = GetInvokingResource() or cache.resource @@ -1348,7 +1301,7 @@ function RemoveMoney(identifier, moneyType, amount, reason) }) emitMoneyEvents(player.PlayerData.source, player.PlayerData.money, moneyType, amount, 'remove', reason) - end + end) return true end @@ -1377,11 +1330,7 @@ function SetMoney(identifier, moneyType, amount, reason) amount = amount }) then return false end - player.PlayerData.money[moneyType] = amount - - if not player.Offline then - UpdatePlayerData(identifier) - + SetPlayerData(identifier, {'money', moneyType}, amount, function() local difference = amount - oldAmount local dirChange = difference < 0 and 'removed' or 'added' local absDifference = math.abs(difference) @@ -1398,7 +1347,7 @@ function SetMoney(identifier, moneyType, amount, reason) }) emitMoneyEvents(player.PlayerData.source, player.PlayerData.money, moneyType, amount, 'set', reason, difference) - end + end) return true end @@ -1490,4 +1439,4 @@ function GenerateUniqueIdentifier(type) return uniqueId end -exports('GenerateUniqueIdentifier', GenerateUniqueIdentifier) +exports('GenerateUniqueIdentifier', GenerateUniqueIdentifier) \ No newline at end of file diff --git a/server/storage/players.lua b/server/storage/players.lua index 3a07f6552..eb189aa62 100644 --- a/server/storage/players.lua +++ b/server/storage/players.lua @@ -1,5 +1,33 @@ local defaultSpawn = require 'config.shared'.defaultSpawn local characterDataTables = require 'config.server'.characterDataTables +local playerDataUpdateQueue = {} +local collectedPlayerData = {} +local isUpdating = false +local isPlayerUpdating = false + +local otherNamedPlayerFields = { + ['items'] = 'inventory', + ['lastLoggedOut'] = 'last_logged_out' +} + +local jsonPlayerFields = { + ['id'] = false, + ['userId'] = false, + ['citizenid'] = false, + ['cid'] = false, + ['license'] = false, + ['name'] = false, + ['money'] = true, + ['charinfo'] = true, + ['job'] = true, + ['gang'] = true, + ['position'] = true, + ['metadata'] = true, + ['inventory'] = true, + ['phone_number'] = false, + ['last_updated'] = false, + ['last_logged_out'] = false +} local function createUsersTable() MySQL.query([[ @@ -382,6 +410,124 @@ local function cleanPlayerGroups() lib.print.info('Removed invalid groups from player_groups table') end +---@param citizenid string +---@param key string | string[] +---@param value any +local function addPlayerDataUpdate(citizenid, key, value) + local hasSubKeys = type(key) == 'table' + + if hasSubKeys then + key[1] = otherNamedPlayerFields[key[1]] or key[1] + else + key = otherNamedPlayerFields[key] or key + end + + if jsonPlayerFields[hasSubKeys and key[1] or key] == nil then + lib.print.error(('Tried to update player data field %s when it doesn\'t exist. Value: %s'):format(hasSubKeys and key[1] or key, value)) + return + end + + if hasSubKeys and not jsonPlayerFields[key[1]] then + lib.print.error(('Tried to update player data field %s as a json object when it isn\'t one'):format(key[1])) + return + end + + value = type(value) == 'table' and json.encode(value) or value + + local currentTable = isUpdating and playerDataUpdateQueue or collectedPlayerData + if not currentTable[citizenid] then + currentTable[citizenid] = {} + end + + currentTable[citizenid][hasSubKeys and key[1] or key] = hasSubKeys and {} or value + + if not hasSubKeys then return end + + local current = currentTable[citizenid][key[1]] + if #key > 2 then + -- We don't check the last one because otherwise we lose the table reference + for i = 2, #key - 1 do + if not current[key[i]] then + current[key[i]] = {} + end + + current = current[key[i]] + end + end + + current[key[#key]] = value +end + +---@param key string +---@param nestedTable table +---@param path string? +---@param citizenid string +local function updateNestedPlayerData(key, nestedTable, citizenid, path) + for k, v in pairs(nestedTable) do + local currentPath = path and ('%s.%s'):format(path, k) or k + if type(v) == 'table' then + updateNestedPlayerData(key, v, citizenid, currentPath) + else + local query = ('UPDATE players SET %s = JSON_SET(%s, "$.%s", ?) WHERE citizenid = ?'):format(key, key, currentPath) + MySQL.prepare.await(query, { v, citizenid }) + end + end +end + +local function sendPlayerDataUpdates() + if isUpdating then return end + + -- We wait on a single player to be updated to not mess with the collectedPlayerData table whilst it's updating + while isPlayerUpdating do + Wait(10) + end + + -- We implement this to ensure when updating no values are added to our updating sequence to prevent data loss by accidentally skipping over it + isUpdating = true + + for citizenid, playerData in pairs(collectedPlayerData) do + for key, data in pairs(playerData) do + if type(data) == 'table' then + updateNestedPlayerData(key, data, citizenid) + else + local query = ('UPDATE players SET %s = ? WHERE citizenid = ?'):format(key) + MySQL.prepare.await(query, { data, citizenid }) + end + end + end + + collectedPlayerData = playerDataUpdateQueue + playerDataUpdateQueue = {} + isUpdating = false +end + +---@param citizenid string +local function forcePlayerDataUpdate(citizenid) + -- We don't need to update a single player when everyone is already getting an update + if isUpdating then return end + + -- We wait on a single player to be updated to not mess with the collectedPlayerData table whilst it's updating + while isPlayerUpdating do + Wait(10) + end + + isPlayerUpdating = true + + local playerData = collectedPlayerData[citizenid] + for key, data in pairs(playerData) do + if type(data) == 'table' then + updateNestedPlayerData(key, data, citizenid) + else + local query = ('UPDATE players SET %s = ? WHERE citizenid = ?'):format(key) + MySQL.prepare.await(query, { data, citizenid }) + end + end + + collectedPlayerData[citizenid] = playerDataUpdateQueue[citizenid] + playerDataUpdateQueue[citizenid] = nil + isPlayerUpdating = false +end + RegisterCommand('cleanplayergroups', function(source) if source ~= 0 then return warn('This command can only be executed using the server console.') end cleanPlayerGroups() @@ -419,4 +565,7 @@ return { removePlayerFromJob = removePlayerFromJob, removePlayerFromGang = removePlayerFromGang, searchPlayerEntities = searchPlayerEntities, + sendPlayerDataUpdates = sendPlayerDataUpdates, + forcePlayerDataUpdate = forcePlayerDataUpdate, + addPlayerDataUpdate = addPlayerDataUpdate } \ No newline at end of file diff --git a/types.lua b/types.lua index bb4fd4628..5f73fcc56 100644 --- a/types.lua +++ b/types.lua @@ -44,7 +44,6 @@ ---@field optin? boolean present if player is online ---@class PlayerFunctions ----@field UpdatePlayerData fun() ---@field SetJob fun(job: string, grade: integer): boolean ---@field SetGang fun(gang: string, grade: integer): boolean ---@field SetJobDuty fun(onDuty: boolean) @@ -102,6 +101,9 @@ ---@field fetchPlayerGroups fun(citizenid: string): table, table jobs, gangs ---@field removePlayerFromJob fun(citizenid: string, group: string) ---@field removePlayerFromGang fun(citizenid: string, group: string) +---@field sendPlayerDataUpdates fun() +---@field forcePlayerDataUpdate fun(citizenid: string) +---@field addPlayerDataUpdate fun(citizenid: string, key: string | string[], value: any) ---@class InsertBanRequest ---@field name string