diff --git a/server/main.lua b/server/main.lua index 80be70c71..b5d267190 100644 --- a/server/main.lua +++ b/server/main.lua @@ -25,7 +25,6 @@ SetRoutingBucketEntityLockdownMode(0, bucketLockDownMode) QBX = {} QBX.Shared = require 'shared.main' ----@alias Source integer ---@type table QBX.Players = {} GlobalState.PlayerCount = 0 diff --git a/server/player.lua b/server/player.lua index e09717ba2..b53657fed 100644 --- a/server/player.lua +++ b/server/player.lua @@ -41,7 +41,7 @@ function Login(source, citizenid, newData) end else local player = CheckPlayerData(source, newData) - player.Functions.Save() + Save(player.PlayerData.source) return true end @@ -61,6 +61,78 @@ end exports('GetOfflinePlayer', GetOfflinePlayer) +---Overwrites current primary job with a new job. Removing the player from their current primary job +---@param identifier Source | string +---@param jobName string name +---@param grade? integer defaults to 0 +---@return boolean success if job was set +---@return ErrorResult? errorResult +function SetJob(identifier, jobName, grade) + jobName = jobName:lower() + grade = tonumber(grade) or 0 + + local job = GetJob(jobName) + + if not job then + 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 + + local player = type(identifier) == 'string' and (GetPlayerByCitizenId(identifier) or GetOfflinePlayer(identifier)) or GetPlayer(identifier) + + if setJobReplaces and player.PlayerData.job.name ~= 'unemployed' then + local success, errorResult = RemovePlayerFromJob(player.PlayerData.citizenid, player.PlayerData.job.name) + + if not success then + return false, errorResult + end + end + + if jobName ~= 'unemployed' then + local success, errorResult = AddPlayerToJob(player.PlayerData.citizenid, jobName, grade) + + if not success then + return false, errorResult + end + end + + local success, errorResult = SetPlayerPrimaryJob(player.PlayerData.citizenid, jobName) + + if not success then + return false, errorResult + end + + return true +end + +exports('SetJob', SetJob) + +---@param identifier Source | string +---@param onDuty boolean +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) + + UpdatePlayerData(identifier) +end + +exports('SetJobDuty', SetJobDuty) + ---@param jobName string ---@param job Job ---@param grade integer @@ -113,10 +185,10 @@ function SetPlayerPrimaryJob(citizenid, jobName) assert(job.grades[grade] ~= nil, ('job %s does not have grade %s'):format(jobName, grade)) player.PlayerData.job = toPlayerJob(jobName, job, grade) - player.Functions.Save() + Save(player.PlayerData.source) if not player.Offline then - player.Functions.UpdatePlayerData() + UpdatePlayerData(player.PlayerData.source) TriggerEvent('QBCore:Server:OnJobUpdate', player.PlayerData.source, player.PlayerData.job) TriggerClientEvent('QBCore:Client:OnJobUpdate', player.PlayerData.source, player.PlayerData.job) end @@ -129,10 +201,13 @@ exports('SetPlayerPrimaryJob', SetPlayerPrimaryJob) ---Adds a player to the job or overwrites their grade for a job already held ---@param citizenid string ---@param jobName string ----@param grade integer +---@param grade? integer ---@return boolean success ---@return ErrorResult? errorResult function AddPlayerToJob(citizenid, jobName, grade) + jobName = jobName:lower() + grade = tonumber(grade) or 0 + -- unemployed job is the default, so players cannot be added to it if jobName == 'unemployed' then return false, { @@ -179,7 +254,7 @@ function AddPlayerToJob(citizenid, jobName, grade) if not player.Offline then player.PlayerData.jobs[jobName] = grade - player.Functions.SetPlayerData('jobs', player.PlayerData.jobs) + SetPlayerData(player.PlayerData.source, '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 @@ -225,11 +300,11 @@ function RemovePlayerFromJob(citizenid, jobName) 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() + Save(player.PlayerData.source) end if not player.Offline then - player.Functions.SetPlayerData('jobs', player.PlayerData.jobs) + SetPlayerData(player.PlayerData.source, 'jobs', player.PlayerData.jobs) TriggerEvent('qbx_core:server:onGroupUpdate', player.PlayerData.source, jobName) TriggerClientEvent('qbx_core:client:onGroupUpdate', player.PlayerData.source, jobName) end @@ -239,12 +314,64 @@ end exports('RemovePlayerFromJob', RemovePlayerFromJob) +---Removes the player from their current primary gang and adds the player to the new gang +---@param identifier Source | string +---@param grade? integer defaults to 0 +---@return boolean success if gang was set +---@return ErrorResult? errorResult +function SetGang(identifier, gangName, grade) + gangName = gangName:lower() + grade = tonumber(grade) or 0 + + local gang = GetGang(gangName) + + if not gang then + 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 + + local player = type(identifier) == 'string' and (GetPlayerByCitizenId(identifier) or GetOfflinePlayer(identifier)) or GetPlayer(identifier) + + if setGangReplaces and player.PlayerData.gang.name ~= 'none' then + local success, errorResult = RemovePlayerFromGang(player.PlayerData.citizenid, player.PlayerData.gang.name) + + if not success then + return false, errorResult + end + end + + if gangName ~= 'none' then + local success, errorResult = AddPlayerToGang(player.PlayerData.citizenid, gangName, grade) + + if not success then + return false, errorResult + end + end + + local success, errorResult = SetPlayerPrimaryGang(player.PlayerData.citizenid, gangName) + + if not success then + return false, errorResult + end + + return true +end + +exports('SetGang', SetGang) + ---Sets a player's gang to be primary only if they already have it. ---@param citizenid string ---@param gangName string ---@return boolean success ---@return ErrorResult? errorResult -local function setPlayerPrimaryGang(citizenid, gangName) +function SetPlayerPrimaryGang(citizenid, gangName) local player = GetPlayerByCitizenId(citizenid) or GetOfflinePlayer(citizenid) if not player then return false, { @@ -281,10 +408,10 @@ local function setPlayerPrimaryGang(citizenid, gangName) } } - player.Functions.Save() + Save(player.PlayerData.source) if not player.Offline then - player.Functions.UpdatePlayerData() + UpdatePlayerData(player.PlayerData.source) TriggerEvent('QBCore:Server:OnGangUpdate', player.PlayerData.source, player.PlayerData.gang) TriggerClientEvent('QBCore:Client:OnGangUpdate', player.PlayerData.source, player.PlayerData.gang) end @@ -292,15 +419,18 @@ local function setPlayerPrimaryGang(citizenid, gangName) return true end -exports('SetPlayerPrimaryGang', setPlayerPrimaryGang) +exports('SetPlayerPrimaryGang', SetPlayerPrimaryGang) ---Adds a player to the gang or overwrites their grade if already in the gang ---@param citizenid string ---@param gangName string ----@param grade integer +---@param grade? integer ---@return boolean success ---@return ErrorResult? errorResult function AddPlayerToGang(citizenid, gangName, grade) + gangName = gangName:lower() + grade = tonumber(grade) or 0 + if gangName == 'none' then return false, { code = 'none', @@ -346,13 +476,13 @@ function AddPlayerToGang(citizenid, gangName, grade) if not player.Offline then player.PlayerData.gangs[gangName] = grade - player.Functions.SetPlayerData('gangs', player.PlayerData.gangs) + SetPlayerData(player.PlayerData.source, '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) + SetPlayerPrimaryGang(citizenid, gangName) end return true @@ -365,7 +495,7 @@ exports('AddPlayerToGang', AddPlayerToGang) ---@param gangName string ---@return boolean success ---@return ErrorResult? errorResult -local function removePlayerFromGang(citizenid, gangName) +function RemovePlayerFromGang(citizenid, gangName) if gangName == 'none' then return false, { code = 'none', @@ -400,11 +530,11 @@ local function removePlayerFromGang(citizenid, gangName) level = 0 } } - player.Functions.Save() + Save(player.PlayerData.source) end if not player.Offline then - player.Functions.SetPlayerData('gangs', player.PlayerData.gangs) + SetPlayerData(player.PlayerData.source, 'gangs', player.PlayerData.gangs) TriggerEvent('qbx_core:server:onGroupUpdate', player.PlayerData.source, gangName) TriggerClientEvent('qbx_core:client:onGroupUpdate', player.PlayerData.source, gangName) end @@ -412,7 +542,7 @@ local function removePlayerFromGang(citizenid, gangName) return true end -exports('RemovePlayerFromGang', removePlayerFromGang) +exports('RemovePlayerFromGang', RemovePlayerFromGang) ---@param source? integer if player is online ---@param playerData? PlayerEntity|PlayerData @@ -557,7 +687,7 @@ function Logout(source) TriggerEvent('QBCore:Server:OnPlayerUnload', source) player.PlayerData.lastLoggedOut = os.time() - player.Functions.Save() + Save(player.PlayerData.source) Wait(200) QBX.Players[source] = nil @@ -580,152 +710,71 @@ function CreatePlayer(playerData, Offline) self.PlayerData = playerData self.Offline = Offline + ---@deprecated use UpdatePlayerData instead function self.Functions.UpdatePlayerData() - if self.Offline then return end -- Unsupported for Offline Players - TriggerEvent('QBCore:Player:SetPlayerData', self.PlayerData) - TriggerClientEvent('QBCore:Player:SetPlayerData', self.PlayerData.source, self.PlayerData) + assert(not self.Offline, 'unsupported for offline players') + + UpdatePlayerData(self.PlayerData.source) end + ---@deprecated use SetJob instead ---Overwrites current primary job with a new job. Removing the player from their current primary job ---@param jobName string name ---@param grade? integer defaults to 0 ---@return boolean success if job was set ---@return ErrorResult? errorResult function self.Functions.SetJob(jobName, grade) - jobName = jobName:lower() - grade = tonumber(grade) or 0 - local job = GetJob(jobName) - if not job then - 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 and self.PlayerData.job.name ~= 'unemployed' then - local success, errorResult = RemovePlayerFromJob(self.PlayerData.citizenid, self.PlayerData.job.name) - if not success then return false, errorResult end - end - if jobName ~= 'unemployed' then - local success, errorResult = AddPlayerToJob(self.PlayerData.citizenid, jobName, grade) - if not success then return false, errorResult end - end - local success, errorResult = SetPlayerPrimaryJob(self.PlayerData.citizenid, jobName) - if not success then return false, errorResult end - return true + return SetJob(self.PlayerData.source, jobName, grade) end + ---@deprecated use SetGang instead ---Removes the player from their current primary gang and adds the player to the new gang ---@param gangName string name ---@param grade? integer defaults to 0 ---@return boolean success if gang was set ---@return ErrorResult? errorResult function self.Functions.SetGang(gangName, grade) - gangName = gangName:lower() - grade = tonumber(grade) or 0 - local gang = GetGang(gangName) - if not gang then - 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 and self.PlayerData.gang.name ~= 'none' then - local success, errorResult = removePlayerFromGang(self.PlayerData.citizenid, self.PlayerData.gang.name) - if not success then return false, errorResult end - end - if gangName ~= 'none' then - local success, errorResult = AddPlayerToGang(self.PlayerData.citizenid, gangName, grade) - if not success then return false, errorResult end - end - local success, errorResult = setPlayerPrimaryGang(self.PlayerData.citizenid, gangName) - if not success then return false, errorResult end - return true + return SetGang(self.PlayerData.source, gangName, grade) end + ---@deprecated use SetJobDuty instead ---@param onDuty boolean function self.Functions.SetJobDuty(onDuty) - self.PlayerData.job.onduty = not not onDuty -- Make sure the value is a boolean if nil is sent - if not self.Offline then - TriggerEvent('QBCore:Server:SetDuty', self.PlayerData.source, self.PlayerData.job.onduty) - TriggerClientEvent('QBCore:Client:SetDuty', self.PlayerData.source, self.PlayerData.job.onduty) - self.Functions.UpdatePlayerData() - end + SetJobDuty(self.PlayerData.source, onDuty) end + ---@deprecated use SetPlayerData instead ---@param key string ---@param val any function self.Functions.SetPlayerData(key, val) - if not key or type(key) ~= 'string' then return end - self.PlayerData[key] = val - self.Functions.UpdatePlayerData() + SetPlayerData(self.PlayerData.source, key, val) end + ---@deprecated use SetMetadata instead ---@param meta string ---@param val any function self.Functions.SetMetaData(meta, val) - if not meta or type(meta) ~= 'string' then return end - - local oldVal = self.PlayerData.metadata[meta] - self.PlayerData.metadata[meta] = val - self.Functions.UpdatePlayerData() - - if not self.Offline then - local playerState = Player(self.PlayerData.source).state - TriggerClientEvent('qbx_core:client:onSetMetaData', self.PlayerData.source, meta, oldVal, val) - TriggerEvent('qbx_core:server:onSetMetaData', meta, oldVal, val, self.PlayerData.source) - - if (meta == 'hunger' or meta == 'thirst' or meta == 'stress') then - val = lib.math.clamp(val, 0, 100) - if playerState[meta] ~= val then - playerState:set(meta, val, true) - end - end - - if (meta == 'dead' or meta == 'inlaststand') then - playerState:set('canUseWeapons', not val, true) - end - end - - if meta == 'inlaststand' or meta == 'isdead' then - self.Functions.Save() - end + SetMetadata(self.PlayerData.source, meta, val) end + ---@deprecated use GetMetadata instead ---@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] + return GetMetadata(self.PlayerData.source, meta) end + ---@deprecated use SetMetadata instead ---@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 - self.Functions.UpdatePlayerData() - end - ---@param moneytype MoneyType - ---@param amount number - ---@param actionType 'add'|'remove'|'set' - ---@param direction boolean - ---@param reason? string - local function emitMoneyEvents(moneytype, amount, actionType, direction, reason) - TriggerClientEvent('hud:client:OnMoneyChange', self.PlayerData.source, moneytype, amount, direction) - TriggerClientEvent('QBCore:Client:OnMoneyChange', self.PlayerData.source, moneytype, amount, actionType, reason) - TriggerEvent('QBCore:Server:OnMoneyChange', self.PlayerData.source, moneytype, amount, actionType, reason) - if moneytype == 'bank' and actionType == 'remove' then - TriggerClientEvent('qb-phone:client:RemoveBankMoney', self.PlayerData.source, amount) - end - local oxmoneytype = moneytype == 'cash' and 'money' or moneytype - if accountsAsItems[oxmoneytype] then - exports.ox_inventory:SetItem(self.PlayerData.source, oxmoneytype, self.PlayerData.money[moneytype]) - end + 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) end ---@param moneytype MoneyType @@ -733,29 +782,7 @@ function CreatePlayer(playerData, Offline) ---@param reason? string ---@return boolean success if money was added 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.Offline then - self.Functions.UpdatePlayerData() - local tags = amount > 100000 and config.logging.role or nil - local resource = GetInvokingResource() or cache.resource - logger.log({ - source = resource, - webhook = config.logging.webhook['playermoney'], - event = 'AddMoney', - color = 'lightgreen', - tags = tags, - message = ('**%s (citizenid: %s | id: %s)** $%s (%s) added, 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), - --oxLibTags = ('script:%s,playerName:%s,citizenId:%s,playerSource:%s,amount:%s,moneyType:%s,newBalance:%s,reason:%s'):format(resource, GetPlayerName(self.PlayerData.source), self.PlayerData.citizenid, self.PlayerData.source, amount, moneytype, self.PlayerData.money[moneytype], reason) - }) - emitMoneyEvents(moneytype, amount, 'add', false, reason) - end - - return true + return AddMoney(self.PlayerData.source, moneytype, amount, reason) end ---@param moneytype MoneyType @@ -763,36 +790,7 @@ function CreatePlayer(playerData, Offline) ---@param reason? string ---@return boolean success if money was removed 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 - for _, mtype in pairs(config.money.dontAllowMinus) do - if mtype == moneytype then - if (self.PlayerData.money[moneytype] - amount) < 0 then - return false - end - end - end - self.PlayerData.money[moneytype] = self.PlayerData.money[moneytype] - amount - - if not self.Offline then - self.Functions.UpdatePlayerData() - local tags = amount > 100000 and config.logging.role or nil - local resource = GetInvokingResource() or cache.resource - logger.log({ - source = resource, - webhook = config.logging.webhook['playermoney'], - event = 'RemoveMoney', - color = 'red', - 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), - --oxLibTags = ('script:%s,playerName:%s,citizenId:%s,playerSource:%s,amount:%s,moneyType:%s,newBalance:%s,reason:%s'):format(resource, GetPlayerName(self.PlayerData.source), self.PlayerData.citizenid, self.PlayerData.source, amount, moneytype, self.PlayerData.money[moneytype], reason) - }) - emitMoneyEvents(moneytype, amount, 'remove', true, reason) - end - - return true + return RemoveMoney(self.PlayerData.source, moneytype, amount, reason) end ---@param moneytype MoneyType @@ -800,39 +798,13 @@ function CreatePlayer(playerData, Offline) ---@param reason? string ---@return boolean success if money was set 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 - local difference = amount - self.PlayerData.money[moneytype] - self.PlayerData.money[moneytype] = amount - - if not self.Offline then - self.Functions.UpdatePlayerData() - local dirChange = difference < 0 and 'removed' or 'added' - local absDifference = math.abs(difference) - local tags = absDifference > 50000 and config.logging.role or {} - local resource = GetInvokingResource() or cache.resource - logger.log({ - source = resource, - webhook = config.logging.webhook['playermoney'], - event = 'SetMoney', - color = difference < 0 and 'red' or 'green', - tags = tags, - message = ('**%s (citizenid: %s | id: %s)** $%s (%s) %s, new %s balance: $%s reason: %s'):format(GetPlayerName(self.PlayerData.source), self.PlayerData.citizenid, self.PlayerData.source, absDifference, moneytype, dirChange, moneytype, self.PlayerData.money[moneytype], reason), - --oxLibTags = ('script:%s,playerName:%s,citizenId:%s,playerSource:%s,amount:%s,moneyType:%s,newBalance:%s,reason:%s,direction:%s'):format(resource, GetPlayerName(self.PlayerData.source), self.PlayerData.citizenid, self.PlayerData.source, absDifference, moneytype, self.PlayerData.money[moneytype], reason, dirChange) - }) - emitMoneyEvents(moneytype, absDifference, 'set', difference < 0, reason) - end - - return true + return SetMoney(self.PlayerData.source, moneytype, amount, reason) end ---@param moneytype MoneyType ---@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] + return GetMoney(self.PlayerData.source, moneytype) end local function qbItemCompat(item) @@ -906,12 +878,16 @@ 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 ---@param cardNumber number function self.Functions.SetCreditCard(cardNumber) self.PlayerData.charinfo.card = cardNumber - self.Functions.UpdatePlayerData() + + ---@diagnostic disable-next-line: param-type-mismatch + UpdatePlayerData(self.Offline and self.PlayerData.citizenid or self.PlayerData.source) end + ---@deprecated use Save or SaveOffline instead function self.Functions.Save() if self.Offline then SaveOffline(self.PlayerData) @@ -949,7 +925,7 @@ function CreatePlayer(playerData, Offline) end if not self.Offline then - self.Functions.UpdatePlayerData() + UpdatePlayerData(self.PlayerData.source) TriggerEvent('QBCore:Server:OnJobUpdate', self.PlayerData.source, self.PlayerData.job) TriggerClientEvent('QBCore:Client:OnJobUpdate', self.PlayerData.source, self.PlayerData.job) end @@ -981,7 +957,7 @@ function CreatePlayer(playerData, Offline) end end if not self.Offline then - self.Functions.UpdatePlayerData() + UpdatePlayerData(self.PlayerData.source) TriggerEvent('QBCore:Server:OnGangUpdate', self.PlayerData.source, self.PlayerData.gang) TriggerClientEvent('QBCore:Client:OnGangUpdate', self.PlayerData.source, self.PlayerData.gang) end @@ -994,7 +970,7 @@ 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 - self.Functions.UpdatePlayerData() + UpdatePlayerData(self.PlayerData.source) Player(self.PlayerData.source).state:set('loadInventory', true, true) TriggerEvent('QBCore:Server:PlayerLoaded', self) end @@ -1060,6 +1036,281 @@ end exports('SaveOffline', SaveOffline) +---@param identifier Source | string +---@param key string +---@param value any +function SetPlayerData(identifier, key, value) + if not key or type(key) ~= 'string' 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 + + UpdatePlayerData(identifier) +end + +---@param identifier Source | string +function UpdatePlayerData(identifier) + local player = type(identifier) == 'string' and (GetPlayerByCitizenId(identifier) or GetOfflinePlayer(identifier)) or GetPlayer(identifier) + + if not player or player.Offline then return end + + TriggerEvent('QBCore:Player:SetPlayerData', player.PlayerData) + TriggerClientEvent('QBCore:Player:SetPlayerData', player.PlayerData.source, player.PlayerData) +end + +---@param identifier Source | string +---@param metadata string +---@param value any +function SetMetadata(identifier, metadata, value) + if not metadata or type(metadata) ~= '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 oldValue = player.PlayerData.metadata[metadata] + + player.PlayerData.metadata[metadata] = value + + UpdatePlayerData(identifier) + + if not player.Offline then + 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) + + if playerState[metadata] ~= value then + playerState:set(metadata, value, true) + end + end + + 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 + +exports('SetMetadata', SetMetadata) + +---@param identifier Source | string +---@param metadata string +---@return any +function GetMetadata(identifier, metadata) + if not metadata or type(metadata) ~= 'string' then return end + + local player = type(identifier) == 'string' and (GetPlayerByCitizenId(identifier) or GetOfflinePlayer(identifier)) or GetPlayer(identifier) + + if not player then return end + + return player.PlayerData.metadata[metadata] +end + +exports('GetMetadata', GetMetadata) + +---@param identifier Source | string +---@param charInfo string +---@param value any +function SetCharInfo(identifier, charInfo, value) + if not charInfo or 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) +end + +exports('SetCharInfo', SetCharInfo) + +---@param source Source +---@param playerMoney table +---@param moneyType MoneyType +---@param amount number +---@param actionType 'add' | 'remove' | 'set' +---@param direction boolean +---@param reason? string +local function emitMoneyEvents(source, playerMoney, moneyType, amount, actionType, direction, reason) + TriggerClientEvent('hud:client:OnMoneyChange', source, moneyType, amount, direction) + TriggerClientEvent('QBCore:Client:OnMoneyChange', source, moneyType, amount, actionType, reason) + TriggerEvent('QBCore:Server:OnMoneyChange', source, moneyType, amount, actionType, reason) + + if moneyType == 'bank' and actionType == 'remove' then + TriggerClientEvent('qb-phone:client:RemoveBankMoney', source, amount) + end + + local oxMoneyType = moneyType == 'cash' and 'money' or moneyType + + if accountsAsItems[oxMoneyType] then + exports.ox_inventory:SetItem(source, oxMoneyType, playerMoney[moneyType]) + end +end + +---@param identifier Source | string +---@param moneyType MoneyType +---@param amount number +---@param reason? string +---@return boolean success if money was added +function AddMoney(identifier, moneyType, amount, reason) + local player = type(identifier) == 'string' and (GetPlayerByCitizenId(identifier) or GetOfflinePlayer(identifier)) or GetPlayer(identifier) + + if not player then return false end + + reason = reason or 'unknown' + amount = qbx.math.round(tonumber(amount) --[[@as number]]) + + if amount < 0 or not player.PlayerData.money[moneyType] then return false end + + player.PlayerData.money[moneyType] += amount + + if not player.Offline then + UpdatePlayerData(identifier) + + local tags = amount > 100000 and config.logging.role or nil + local resource = GetInvokingResource() or cache.resource + + logger.log({ + source = resource, + webhook = config.logging.webhook['playermoney'], + event = 'AddMoney', + color = 'lightgreen', + tags = tags, + message = ('**%s (citizenid: %s | id: %s)** $%s (%s) added, new %s balance: $%s reason: %s'):format(GetPlayerName(player.PlayerData.source), player.PlayerData.citizenid, player.PlayerData.source, amount, moneyType, moneyType, player.PlayerData.money[moneyType], reason), + --oxLibTags = ('script:%s,playerName:%s,citizenId:%s,playerSource:%s,amount:%s,moneyType:%s,newBalance:%s,reason:%s'):format(resource, GetPlayerName(player.PlayerData.source), player.PlayerData.citizenid, player.PlayerData.source, amount, moneyType, player.PlayerData.money[moneyType], reason) + }) + + emitMoneyEvents(player.PlayerData.source, player.PlayerData.money, moneyType, amount, 'add', false, reason) + end + + return true +end + +exports('AddMoney', AddMoney) + +---@param identifier Source | string +---@param moneyType MoneyType +---@param amount number +---@param reason? string +---@return boolean success if money was removed +function RemoveMoney(identifier, moneyType, amount, reason) + local player = type(identifier) == 'string' and (GetPlayerByCitizenId(identifier) or GetOfflinePlayer(identifier)) or GetPlayer(identifier) + + if not player then return false end + + reason = reason or 'unknown' + amount = qbx.math.round(tonumber(amount) --[[@as number]]) + + if amount < 0 or not player.PlayerData.money[moneyType] then return false end + + for _, mType in pairs(config.money.dontAllowMinus) do + if mType == moneyType then + if (player.PlayerData.money[moneyType] - amount) < 0 then + return false + end + end + end + + player.PlayerData.money[moneyType] -= amount + + if not player.Offline then + UpdatePlayerData(identifier) + + local tags = amount > 100000 and config.logging.role or nil + local resource = GetInvokingResource() or cache.resource + + logger.log({ + source = resource, + webhook = config.logging.webhook['playermoney'], + event = 'RemoveMoney', + color = 'red', + tags = tags, + message = ('** %s (citizenid: %s | id: %s)** $%s (%s) removed, new %s balance: $%s reason: %s'):format(GetPlayerName(player.PlayerData.source), player.PlayerData.citizenid, player.PlayerData.source, amount, moneyType, moneyType, player.PlayerData.money[moneyType], reason), + --oxLibTags = ('script:%s,playerName:%s,citizenId:%s,playerSource:%s,amount:%s,moneyType:%s,newBalance:%s,reason:%s'):format(resource, GetPlayerName(player.PlayerData.source), player.PlayerData.citizenid, player.PlayerData.source, amount, moneyType, player.PlayerData.money[moneyType], reason) + }) + + emitMoneyEvents(player.PlayerData.source, player.PlayerData.money, moneyType, amount, 'remove', false, reason) + end + + return true +end + +exports('RemoveMoney', RemoveMoney) + +---@param identifier Source | string +---@param moneyType MoneyType +---@param amount number +---@param reason? string +---@return boolean success if money was set +function SetMoney(identifier, moneyType, amount, reason) + local player = type(identifier) == 'string' and (GetPlayerByCitizenId(identifier) or GetOfflinePlayer(identifier)) or GetPlayer(identifier) + + if not player then return false end + + reason = reason or 'unknown' + amount = qbx.math.round(tonumber(amount) --[[@as number]]) + + if amount < 0 or not player.PlayerData.money[moneyType] then return false end + + player.PlayerData.money[moneyType] = amount + + if not player.Offline then + UpdatePlayerData(identifier) + + local difference = amount - player.PlayerData.money[moneyType] + local dirChange = difference < 0 and 'removed' or 'added' + local absDifference = math.abs(difference) + local tags = absDifference > 50000 and config.logging.role or {} + local resource = GetInvokingResource() or cache.resource + + logger.log({ + source = resource, + webhook = config.logging.webhook['playermoney'], + event = 'SetMoney', + color = difference < 0 and 'red' or 'green', + tags = tags, + message = ('**%s (citizenid: %s | id: %s)** $%s (%s) %s, new %s balance: $%s reason: %s'):format(GetPlayerName(player.PlayerData.source), player.PlayerData.citizenid, player.PlayerData.source, absDifference, moneyType, dirChange, moneyType, player.PlayerData.money[moneyType], reason), + --oxLibTags = ('script:%s,playerName:%s,citizenId:%s,playerSource:%s,amount:%s,moneyType:%s,newBalance:%s,reason:%s,direction:%s'):format(resource, GetPlayerName(player.PlayerData.source), player.PlayerData.citizenid, player.PlayerData.source, absDifference, moneyType, player.PlayerData.money[moneyType], reason, dirChange) + }) + end + + return true +end + +exports('SetMoney', SetMoney) + +---@param identifier Source | string +---@param moneyType MoneyType +---@return boolean | number amount or false if moneytype does not exist +function GetMoney(identifier, moneyType) + if not moneyType then return false end + + local player = type(identifier) == 'string' and (GetPlayerByCitizenId(identifier) or GetOfflinePlayer(identifier)) or GetPlayer(identifier) + + if not player then return false end + + return player.PlayerData.money[moneyType] +end + +exports('GetMoney', GetMoney) + ---@param source Source ---@param citizenid string function DeleteCharacter(source, citizenid) diff --git a/types.lua b/types.lua index 8ec4de1fc..d12cccb7f 100644 --- a/types.lua +++ b/types.lua @@ -2,6 +2,7 @@ ---@alias NotificationPosition 'top' | 'top-right' | 'top-left' | 'bottom' | 'bottom-right' | 'bottom-left' | 'center-right' | 'center-left' ---@alias NotificationType 'info' | 'warning' | 'success' | 'error' +---@alias Source integer ---@class ErrorResult ---@field code string