475 lines
17 KiB
Lua
475 lines
17 KiB
Lua
-- ==========================================================
|
||
-- 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
|