-- ========================================================== -- ESX Bridge (QBCore -> ESX) for codem-phone -- Mirrors your Core API and behaviors as close as possible. -- ========================================================== if Config.Framework ~= "esx" then return end local function CheckESXStatus() local state = GetResourceState('es_extended') return state == "started" or state == "starting" end local nameCache = {} Core = { CoreReady = false, JobbyJobs = {}, JobbyJobsReady = false, } Core.__index = Core Core.Functions = {} local databaseReady = false AddEventHandler('codem-phone:database:ready', function() databaseReady = true end) local ESX = nil if CheckESXStatus() then local ok, res = pcall(function() return exports['es_extended']:getSharedObject() end) if ok and res then ESX = res Core.CoreReady = true print('^2[codem-phone] ^7ESX object retrieved successfully') Citizen.CreateThread(function() local timeout = 0 while not databaseReady and timeout < 60000 do Wait(100) timeout = timeout + 100 end if not databaseReady then print('^1[CODEM-PHONE] Framework ERROR: Database initialization timeout!^7') return end local JobbyConfig = LoadFile and LoadFile('config/AppConfig/JobbyConfig.lua') or nil local players = MySQL.query.await([[ SELECT identifier, job, job_grade, firstname, lastname FROM users ]]) local jobsRaw = MySQL.query.await("SELECT name, label FROM jobs") local jobGradesRaw = MySQL.query.await("SELECT job_name, grade, label, salary, name FROM job_grades") local JobsIndex = {} for _, j in ipairs(jobsRaw or {}) do JobsIndex[j.name] = { label = j.label, grades = {} } end for _, g in ipairs(jobGradesRaw or {}) do local J = JobsIndex[g.job_name] if J then J.grades[tostring(g.grade)] = { name = g.label or g.name or ('Grade ' .. g.grade), payment = g.salary or 0, isboss = false, } end end for jobName, _ in pairs(JobbyConfig and JobbyConfig.AllowJobs or {}) do Core.JobbyJobs[jobName] = { label = (JobsIndex[jobName] and JobsIndex[jobName].label) or 'Unknown Job', players = {}, name = jobName, grades = (JobsIndex[jobName] and JobsIndex[jobName].grades) or {}, money = 0, announcements = {}, logs = {}, } local hasAnn = MySQL.scalar.await( "SELECT 1 FROM information_schema.tables WHERE table_name = 'codem_mphone_jobby_announcements' LIMIT 1") if hasAnn then Core.JobbyJobs[jobName].announcements = MySQL.query.await( [[SELECT id, title, content, created_at FROM codem_mphone_jobby_announcements WHERE jobname = ? ORDER BY created_at DESC LIMIT 15]], { jobName }) or {} end local hasLogs = MySQL.scalar.await( "SELECT 1 FROM information_schema.tables WHERE table_name = 'codem_mphone_jobby_log' LIMIT 1") if hasLogs then Core.JobbyJobs[jobName].logs = MySQL.query.await( [[SELECT id, action, amount, playername, phone_number, date FROM codem_mphone_jobby_log WHERE jobname = ? ORDER BY date DESC LIMIT 20]], { jobName }) or {} end end for _, u in ipairs(players or {}) do local jname = u.job if Core.JobbyJobs[jname] then local gradeLevel = tonumber(u.job_grade) or 0 local gradeName = (JobsIndex[jname] and JobsIndex[jname].grades and JobsIndex[jname].grades[tostring(gradeLevel)] and JobsIndex[jname].grades[tostring(gradeLevel)].name) or 'Unknown Grade' local playerName = ((u.firstname or '') .. ' ' .. (u.lastname or '')) :gsub("^%l", string.upper) Core.JobbyJobs[jname].players[#Core.JobbyJobs[jname].players + 1] = { name = playerName ~= ' ' and playerName or 'Unknown Player', grade_level = gradeLevel, grade_name = gradeName, src = false, identifier = u.identifier, boss = false, } end end for jobName, data in pairs(Core.JobbyJobs) do Core.JobbyJobs[jobName].money = GetJobMoney and GetJobMoney(jobName) or 0 end Core.JobbyJobsReady = true end) else print('^1[codem-phone] ^7Error: ESX object could not be retrieved') return end else print('^1[codem-phone] ^7Error: es_extended not found or not started') return end local function getXPlayer(playerId) if not playerId then return nil end return ESX.GetPlayerFromId(playerId) end function GetHighestGrade(job) local AllFrameworkJobs = Core.JobbyJobs or {} if AllFrameworkJobs[job] then local AllGrades = AllFrameworkJobs[job].grades or {} local highest = -1 for key, _ in pairs(AllGrades) do local n = tonumber(key) if n and n > highest then highest = n end end return highest end return false end function IsBoss(job, gradeLevel) local highestGrade = GetHighestGrade(job) return highestGrade and gradeLevel >= highestGrade end Core.Functions.GetPlayer = function(playerId) if not playerId then -- DebugPrint and DebugPrint('Error: playerId is required') return nil end local xPlayer = getXPlayer(playerId) if xPlayer then return xPlayer end -- DebugPrint and DebugPrint('Error: Player not found - ID: ' .. tostring(playerId)) return nil end Core.Functions.GetIdentifier = function(playerId) if not playerId then -- DebugPrint and DebugPrint('Error: playerId is required') return nil end local xPlayer = getXPlayer(playerId) if xPlayer then return xPlayer.identifier end --DebugPrint and DebugPrint('Error: Player not found - ID: ' .. tostring(playerId)) return nil end Core.Functions.GetSourceFromIdentifier = function(identifier) if not identifier then --- DebugPrint and DebugPrint('Error: identifier is required') return nil end if ESX.GetPlayerFromIdentifier then local xPlayer = ESX.GetPlayerFromIdentifier(identifier) return xPlayer and xPlayer.source or nil end for _, src in ipairs(GetPlayers()) do local xP = ESX.GetPlayerFromId(tonumber(src)) if xP and xP.identifier == identifier then return xP.source end end -- DebugPrint and DebugPrint('Error: Player not online for identifier - ' .. tostring(identifier)) return nil end Core.Functions.GetPlayerJob = function(playerId) if not playerId then -- DebugPrint and DebugPrint('Error: playerId is required') return nil end local xPlayer = getXPlayer(playerId) if not xPlayer then -- DebugPrint and DebugPrint('Error: Player not found - ID: ' .. tostring(playerId)) return nil end local job = xPlayer.getJob() if not job then return { name = 'unemployed', label = 'Unemployed', onduty = true, grade_name = 'unemployed', grade_level = 0, isboss = false } end return { name = job.name or 'unemployed', label = job.label or 'Unemployed', onduty = true, grade_name = job.grade_label or (job.grade and tostring(job.grade)) or 'unemployed', grade_level = job.grade or 0, isboss = (job.grade_name == 'boss') or (job.grade_label == 'boss') or false, } end Core.Functions.IsPlayerAdmin = function(playerId) if not playerId then --DebugPrint and DebugPrint('Error: playerId is required') return false end local xPlayer = getXPlayer(playerId) if not xPlayer then return false end local grp = (xPlayer.getGroup and xPlayer.getGroup()) or xPlayer.group or 'user' local allowed = Config.AdminPermissions or { 'admin', 'superadmin' } for _, g in ipairs(allowed) do if grp == g then return true end end if IsPlayerAceAllowed(playerId, 'command') then return true end return false end AddEventHandler('esx:setJob', function(src, job) if type(src) ~= 'number' then src = source end if not src or not job then return end TriggerClientEvent('codem-phone:client:OnJobUpdate', src) OnJobUpdate(src, { jobname = job.name or 'unemployed', onduty = true, }) OnJobbyJobUpdate(src) end) Core.Functions.GetName = function(playerIdOrIdentifier, nameType) if not playerIdOrIdentifier then -- DebugPrint and DebugPrint('Error: playerId is required') return 'Unknown' end -- online? local asNumber = tonumber(playerIdOrIdentifier) if asNumber then local xPlayer = getXPlayer(asNumber) if xPlayer then local first = xPlayer.get('firstName') or xPlayer.get('firstname') or "" local last = xPlayer.get('lastName') or xPlayer.get('lastname') or "" if (first == "" and last == "") and xPlayer.getName then local n = xPlayer.getName() if nameType and nameType:lower() == 'first' then return (n:match("^(%S+)") or n) elseif nameType and nameType:lower() == 'full' then return n end return n end if nameType and nameType:lower() == 'first' then return (first ~= "" and first or 'Unknown'):gsub("^%l", string.upper) elseif nameType and nameType:lower() == 'full' then local full = (first .. " " .. last):gsub("^%l", string.upper) return (full ~= " " and full or 'Unknown') end local full = (first .. " " .. last):gsub("^%l", string.upper) return (full ~= " " and full or 'Unknown') end -- fall through to offline using identifier cache (we don’t have it here) return 'Unknown' else -- treat param as identifier (offline support) local identifier = tostring(playerIdOrIdentifier) if nameCache[identifier] then local cached = nameCache[identifier] if nameType and nameType:lower() == 'first' then return (cached.firstname):gsub("^%l", string.upper) elseif nameType and nameType:lower() == 'full' then return (cached.firstname .. " " .. cached.lastname):gsub("^%l", string.upper) end return (cached.firstname .. " " .. cached.lastname):gsub("^%l", string.upper) end -- DB lookup (ESX default schema) local row = MySQL.single.await("SELECT firstname, lastname FROM users WHERE identifier = ? LIMIT 1", { identifier }) if row then nameCache[identifier] = { firstname = row.firstname or "Unknown", lastname = row.lastname or "" } if nameType and nameType:lower() == 'first' then return (row.firstname or "Unknown"):gsub("^%l", string.upper) elseif nameType and nameType:lower() == 'full' then return ((row.firstname or "") .. " " .. (row.lastname or "")):gsub("^%l", string.upper) end return ((row.firstname or "") .. " " .. (row.lastname or "")):gsub("^%l", string.upper) end return 'Unknown' end end -- Money getters in ESX: use accounts (bank/money/black_money) Core.Functions.GetMoney = function(playerId, moneyType) if not playerId then --DebugPrint and DebugPrint('Error: playerId is required') return 0 end moneyType = moneyType or 'bank' local xPlayer = getXPlayer(playerId) if not xPlayer then return { success = false, message = 'Error: Player not found - ID: ' .. tostring(playerId) } end local acc = xPlayer.getAccount(moneyType) local amount = acc and acc.money or 0 return { success = true, money = amount } end Core.Functions.RemoveMoney = function(playerId, amount, moneyType) if not playerId then -- DebugPrint and DebugPrint('Error: playerId is required') return false end amount = tonumber(amount) if not amount or amount <= 0 then -- DebugPrint and DebugPrint('Error: amount must be positive') return false end moneyType = moneyType or 'bank' local xPlayer = getXPlayer(playerId) if not xPlayer then -- DebugPrint and DebugPrint('Error: Player not found - ID: ' .. tostring(playerId)) return false end local acc = xPlayer.getAccount(moneyType) local bal = acc and acc.money or 0 if bal < amount then -- DebugPrint and DebugPrint(('Error: Insufficient funds (%s < %s) - ID: %s'):format(bal, amount, tostring(playerId))) return false end xPlayer.removeAccountMoney(moneyType, amount, "codem-phone") return true end Core.Functions.AddMoney = function(playerId, amount, moneyType) if not playerId then -- DebugPrint and DebugPrint('Error: playerId is required') return false end amount = tonumber(amount) if not amount or amount <= 0 then -- DebugPrint and DebugPrint('Error: amount must be positive') return false end moneyType = moneyType or 'bank' local xPlayer = getXPlayer(playerId) if not xPlayer then -- DebugPrint and DebugPrint('Error: Player not found - ID: ' .. tostring(playerId)) return false end xPlayer.addAccountMoney(moneyType, amount, "codem-phone") return true end -- Offline money update (users.accounts JSON) Core.Functions.AddMoneyOffline = function(identifier, amount, moneyType) if not identifier then return { success = false, message = "Identifier is required." } end amount = tonumber(amount) if not amount or amount <= 0 then return { success = false, message = "Invalid amount." } end moneyType = moneyType or 'bank' -- get accounts JSON local accountsJson = MySQL.scalar.await("SELECT accounts FROM users WHERE identifier = ?", { identifier }) if not accountsJson then return { success = false, message = "Player not found in database." } end local acc = {} if type(accountsJson) == "string" and accountsJson ~= "" then acc = json.decode(accountsJson) or {} elseif type(accountsJson) == "table" then acc = accountsJson end acc[moneyType] = (tonumber(acc[moneyType]) or 0) + amount local rows = MySQL.update.await("UPDATE users SET accounts = ? WHERE identifier = ?", { json.encode(acc), identifier }) if rows and rows > 0 then return { success = true } else return { success = false, message = "Failed to update money for offline player." } end end Core.Functions.SetPlayerJob = function(playerId, jobname, grade) local xPlayer = getXPlayer(playerId) if not xPlayer then -- DebugPrint and DebugPrint('Error: Player not found - ID: ' .. tostring(playerId)) return false end local exists = MySQL.scalar.await("SELECT 1 FROM jobs WHERE name = ? LIMIT 1", { jobname }) if not exists then -- DebugPrint and DebugPrint(('Error: job "%s" not found in DB'):format(jobname)) return false end xPlayer.setJob(jobname, tonumber(grade) or 0) return true end Core.Functions.CreateUseableItem = function(itemName, callback) if not itemName or type(itemName) ~= 'string' then -- DebugPrint and DebugPrint('Error: itemName must be a valid string') return end if not callback or type(callback) ~= 'function' then -- DebugPrint and DebugPrint('Error: callback must be a valid function') return end if ESX.RegisterUsableItem then ESX.RegisterUsableItem(itemName, function(source, item) callback(source, item) end) else -- DebugPrint and DebugPrint('Error: ESX.RegisterUsableItem not available in this build') end end