From 11052e2aacdf7909198d432863e9a4af8640fe20 Mon Sep 17 00:00:00 2001 From: Kakarot <57848836+GhzGarage@users.noreply.github.com> Date: Tue, 11 Mar 2025 23:32:11 -0500 Subject: [PATCH] wip player creation with state and backward compat --- server/player.lua | 343 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 343 insertions(+) create mode 100644 server/player.lua diff --git a/server/player.lua b/server/player.lua new file mode 100644 index 0000000..db036d5 --- /dev/null +++ b/server/player.lua @@ -0,0 +1,343 @@ +-- Backwards Compat + +local function extractData(source, prefix) + local state = Player(source).state + local data = {} + + for key, value in pairs(state) do + if key:find('^' .. prefix) then + local shortKey = key:gsub(prefix, '') + data[shortKey] = value or 'Unknown' + end + end + + return data +end + +local function CreateLegacyPlayerData(player) + local source = player.source + local state = Player(source).state + local legacy = {} + legacy.license = QBCore.Functions.GetIdentifier(source, 'license') + legacy.source = source + legacy.name = GetPlayerName(source) + legacy.citizenid = state['citizenid'] + legacy.money = extractData(source, 'money:') + legacy.job = extractData(source, 'job:') + legacy.gang = extractData(source, 'gang:') + legacy.metadata = extractData(source, 'metadata:') + legacy.charinfo = extractData(source, 'charinfo:') + legacy.position = extractData(source, 'position:') + + setmetatable(legacy, { + __index = function(tbl, key) + if key == 'money' then + return extractData(source, 'money:') + elseif key == 'job' then + return extractData(source, 'job:') + elseif key == 'gang' then + return extractData(source, 'gang:') + elseif key == 'metadata' then + return extractData(source, 'metadata:') + elseif key == 'charinfo' then + return extractData(source, 'charinfo:') + elseif key == 'position' then + return extractData(source, 'position:') + else + return rawget(tbl, key) + end + end, + __newindex = function(tbl, key, value) + local state = Player(source).state + if key == 'money' and type(value) == 'table' then + for subkey, subvalue in pairs(value) do + state:set('money:' .. subkey, subvalue, true) + end + elseif key == 'job' and type(value) == 'table' then + for subkey, subvalue in pairs(value) do + state:set('job:' .. subkey, subvalue, true) + end + elseif key == 'gang' and type(value) == 'table' then + for subkey, subvalue in pairs(value) do + state:set('gang:' .. subkey, subvalue, true) + end + elseif key == 'metadata' and type(value) == 'table' then + for subkey, subvalue in pairs(value) do + state:set('metadata:' .. subkey, subvalue, true) + end + elseif key == 'charinfo' and type(value) == 'table' then + for subkey, subvalue in pairs(value) do + state:set('charinfo:' .. subkey, subvalue, true) + end + elseif key == 'position' and type(value) == 'table' then + for subkey, subvalue in pairs(value) do + state:set('position:' .. subkey, subvalue, true) + end + else + rawset(tbl, key, value) + end + end, + }) + return legacy +end + +-- JSON STUFF + +local DynamicDefaults = { + ['citizenid'] = QBCore.Functions.CreateCitizenId, + ['charinfo.phone'] = QBCore.Functions.CreatePhoneNumber, + ['charinfo.account'] = QBCore.Functions.CreateAccountNumber, + ['metadata.bloodtype'] = function() return QBCore.Functions.GetRandomElement(QBCore.Config.Player.Bloodtypes) end, + ['metadata.fingerprint'] = QBCore.Functions.CreateFingerId, + ['metadata.walletid'] = QBCore.Functions.CreateWalletId +} + +local function ApplyDynamicDefaults(target) + for field, func in pairs(DynamicDefaults) do + local ref = target + local keys = {} + for key in field:gmatch('[^.]+') do table.insert(keys, key) end + + for i = 1, #keys - 1 do + ref[keys[i]] = ref[keys[i]] or {} + ref = ref[keys[i]] + end + + ref[keys[#keys]] = ref[keys[#keys]] or func() + end +end + +local function LoadPlayerDefaults() + local success, defaults = pcall(function() + return json.decode(LoadResourceFile(resourceName, 'shared/player_defaults.json')) + end) + + if not success or not defaults or not next(defaults) then + print('^1[ERROR]^0 Could not load player_defaults.json. Ensure the file is valid JSON and not empty.') + return {} + end + + ApplyDynamicDefaults(defaults) + return defaults +end + +local function MergePlayerData(target, defaults, debug) + for key, value in pairs(defaults) do + if type(value) == 'table' then + target[key] = target[key] or {} + MergePlayerData(target[key], value, debug) + else + if not debug then + if target[key] == nil then + print(('^2[INFO]^0 Added new data field: %s = %s'):format(key, tostring(value))) + elseif target[key] ~= value then + print(('^3[INFO]^0 Updated data field: %s = %s'):format(key, tostring(value))) + end + end + target[key] = (target[key] == nil or target[key] == '') and value or target[key] + end + end +end + +-- Player Creation + +local function InitializePlayerStateBag(source, playerData) + local state = Player(source).state + local defaults = LoadPlayerDefaults() + + for _, key in pairs(GetStateBagKeys('player:' .. source)) do + if not defaults[key] then + print(('^1[INFO]^0 Removed outdated data field: %s'):format(key)) + end + state:set(key, nil, true) + end + + MergePlayerData(playerData, defaults) + + local function populatePlayerState(data, prefix) + for key, value in pairs(data) do + if type(value) == 'table' then + populatePlayerState(value, prefix .. key .. ':') + else + state:set(prefix .. key, value, true) + end + end + end + + populatePlayerState(playerData, '') +end + +function QBCore.Player.Login(source, citizenid, newData) + if not source or source == '' then + QBCore.ShowError(resourceName, 'ERROR QBCORE.PLAYER.LOGIN - NO SOURCE GIVEN!') + return false + end + + if citizenid then + local license = QBCore.Functions.GetIdentifier(source, 'license') + local data = MySQL.prepare.await('SELECT * FROM players WHERE citizenid = ?', { citizenid }) + + if not data then + print('^1[ERROR]^0 Failed to load data for Citizen ID:', citizenid) + DropPlayer(source, 'Failed to load your character data. Please contact staff.') + return false + end + + if data and license == data.license then + local success, playerData = pcall(function() + return { + money = json.decode(data.money) or {}, + job = json.decode(data.job) or {}, + gang = json.decode(data.gang) or {}, + position = json.decode(data.position) or {}, + metadata = json.decode(data.metadata) or {}, + charinfo = json.decode(data.charinfo) or {} + } + end) + + if not success then + print('^1[ERROR]^0 Failed to decode player data for Citizen ID:', citizenid) + DropPlayer(source, 'Failed to decode your character data. Please contact staff.') + return false + end + + InitializePlayerStateBag(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) + end + else + InitializePlayerStateBag(source, newData or {}) + end + + return true +end + +function QBCore.Player.CreatePlayer(PlayerData, Offline) + local self = {} + self.Offline = Offline + self.Functions = {} + + function self.Functions.Notify(text, type, length) + TriggerClientEvent('QBCore:Notify', PlayerData.source, text, type, length) + end + + function self.Functions.AddMethod(methodName, handler) + self.Functions[methodName] = handler + end + + function self.Functions.AddField(fieldName, data) + self[fieldName] = data + local state = Player(PlayerData.source).state + state:set(fieldName, data, true) + end + + local function CreateDynamicGetSet(prefix) + return { + Get = function(key) + local state = Player(PlayerData.source).state + return state[prefix .. key] or nil + end, + + Set = function(key, value) + local state = Player(PlayerData.source).state + state:set(prefix .. key, value, true) + return true + end + } + end + + local function CreateDynamicIncremental(prefix) + return { + Get = function(key) + local state = Player(PlayerData.source).state + return state[prefix .. key] or 0 + end, + + Set = function(key, value) + local state = Player(PlayerData.source).state + state:set(prefix .. key, value, true) + return true + end, + + Add = function(key, value) + local state = Player(PlayerData.source).state + local currentValue = state[prefix .. key] or 0 + state:set(prefix .. key, currentValue + value, true) + return true + end, + + Remove = function(key, value) + local state = Player(PlayerData.source).state + local currentValue = state[prefix .. key] or 0 + if currentValue < value then return false end + state:set(prefix .. key, currentValue - value, true) + return true + end + } + end + + self.Metadata = CreateDynamicGetSet('metadata:') + self.Job = CreateDynamicGetSet('job:') + self.Gang = CreateDynamicGetSet('gang:') + self.Money = CreateDynamicIncremental('money:') + self.CharInfo = CreateDynamicGetSet('charinfo:') + self.Position = CreateDynamicGetSet('position:') + self.PlayerData = CreateLegacyPlayerData(PlayerData) + + if not self.Offline then + QBCore.Players[PlayerData.source] = self + QBCore.Player.Save(PlayerData.source) + TriggerEvent('QBCore:Server:PlayerLoaded', self) + end + + return self +end + +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 + + InitializePlayerStateBag(source, PlayerData) + + return QBCore.Player.CreatePlayer(PlayerData, Offline) +end + +local function BuildPlayerSaveData(source) + local state = Player(source).state + + return { + citizenid = state['citizenid'], + money = json.encode(extractData(source, 'money:')), + metadata = json.encode(extractData(source, 'metadata:')), + position = json.encode(extractData(source, 'position:')), + job = json.encode(extractData(source, 'job:')), + gang = json.encode(extractData(source, 'gang:')) + } +end + +function QBCore.Player.Save(source) + local saveData = BuildPlayerSaveData(source) + + if not saveData.citizenid or not saveData.license then + print('^1[ERROR]^0 Skipped saving data for player [%s] due to missing critical data.'):format(GetPlayerName(source)) + return + end + + MySQL.insert( + 'INSERT INTO players (citizenid, money, metadata, position, job, gang) VALUES (:citizenid, :money, :metadata, :position, :job, :gang) ON DUPLICATE KEY UPDATE money = :money, metadata = :metadata, position = :position, job = :job, gang = :gang', + saveData + ) + + if GetResourceState('qb-inventory') ~= 'missing' then + exports['qb-inventory']:SaveInventory(source) + end + + QBCore.ShowSuccess(resourceName, 'Player ' .. GetPlayerName(source) .. ' data saved successfully.') +end