sv_utils.lua
CORE = exports["ak4y-core"]
FrameworkName = nil
Framework = nil
PhotoWebhook = ""
CreateThread(function()
if not FrameworkName then
if GetResourceState("qbx_core") == "started" then
FrameworkName = "qb"
Framework = exports['qb-core']:GetCoreObject()
elseif GetResourceState("es_extended") == "started" then
FrameworkName = "esx"
Framework = exports['es_extended']:getSharedObject()
elseif GetResourceState("qb-core") == "started" then
FrameworkName = "qb"
Framework = exports['qb-core']:GetCoreObject()
end
end
end)
CORE:Register('ak4y-multicharacter-v3:GetSkinData', function(source, cid)
local src = source
local skinData = nil
if FrameworkName == "esx" then
-- ESX: Return skin as string (same as old script line 32-33)
skinData = CORE:ExecuteSql("SELECT skin FROM users WHERE identifier = '" .. cid .. "'")
if skinData[1] and skinData[1].skin then
return skinData[1].skin -- Return string, client will decode
end
return nil
else
-- QB: Return table array
skinData = CORE:ExecuteSql("SELECT * FROM playerskins WHERE citizenid = '" .. cid .. "' AND active = 1")
return skinData
end
end)
CORE:Register('ak4y-multicharacter-v3:GetPhotoWebhook', function(source)
return PhotoWebhook or ""
end)
CreateThread(function()
local hasDonePreloading = {}
local awaitingRegistration = {}
local playerIdentity = {}
local playerLoginTime = {}
if FrameworkName == "qb" then
AddEventHandler('QBCore:Server:PlayerLoaded', function(Player)
Wait(1000)
hasDonePreloading[Player.PlayerData.source] = true
local citizenid = Player.PlayerData.citizenid
if citizenid then
playerLoginTime[citizenid] = os.time()
end
end)
AddEventHandler('QBCore:Server:OnPlayerUnload', function(src)
hasDonePreloading[src] = false
end)
AddEventHandler('playerDropped', function(reason)
local src = source
if Framework then
local Player = Framework.Functions.GetPlayer(src)
if Player then
local citizenid = Player.PlayerData.citizenid
if citizenid and playerLoginTime[citizenid] then
local loginTime = playerLoginTime[citizenid]
local currentTime = os.time()
local playtimeSeconds = currentTime - loginTime
local result = CORE:ExecuteSql("SELECT playtime FROM ak4y_multicharacter_v3 WHERE identifier = '" .. citizenid .. "'")
local currentPlaytime = 0
if result[1] and result[1].playtime then
currentPlaytime = tonumber(result[1].playtime) or 0
end
local newPlaytime = currentPlaytime + playtimeSeconds
CORE:ExecuteSql("UPDATE ak4y_multicharacter_v3 SET playtime = " .. newPlaytime .. " WHERE identifier = '" .. citizenid .. "'")
playerLoginTime[citizenid] = nil
end
end
end
end)
RegisterNetEvent('ak4y-multicharacter-v3:server:loadUserData', function(cData)
print("loadUserData", cData)
local src = source
if Framework then
if Framework.Player.Login(src, cData) then
repeat
Wait(10)
until hasDonePreloading[src]
print('^2[qb-core]^7 '..GetPlayerName(src)..' (Citizen ID: '..cData..') has succesfully loaded!')
Framework.Commands.Refresh(src)
if Config.LastPositionLoad then
local Player = Framework.Functions.GetPlayer(src)
local spawnCoords = Config.DefaultSpawn
if Player and Player.PlayerData and Player.PlayerData.position then
local position = Player.PlayerData.position
if type(position) == "string" then
position = json.decode(position)
end
if position and position.x and position.y and position.z then
spawnCoords = vector3(position.x, position.y, position.z)
if position.w or position.heading then
spawnCoords = vector4(position.x, position.y, position.z, position.w or position.heading or 0.0)
end
end
end
TriggerClientEvent('ak4y-multicharacter-v3:client:closeNUIdefault', src, spawnCoords)
else
if Config.SpawnSelector then
Config.SpawnSelector(src, cData, "qb")
else
TriggerClientEvent('ak4y-multicharacter-v3:client:closeNUIdefault', src, Config.DefaultSpawn)
end
end
TriggerClientEvent("ak4y-multicharacter-v3:client:closeNUI", src)
end
end
end)
end
if FrameworkName == "esx" then
AddEventHandler('esx:playerLoaded', function(source, xPlayer)
local identifier = xPlayer.identifier
if identifier then
playerLoginTime[identifier] = os.time()
end
end)
AddEventHandler('playerDropped', function(reason)
local src = source
awaitingRegistration[src] = nil
if Framework and Framework.Players then
Framework.Players[GetIdentifierForESX(src)] = nil
end
if Framework then
local xPlayer = Framework.GetPlayerFromId(src)
if xPlayer then
local identifier = xPlayer.identifier
if identifier and playerLoginTime[identifier] then
local loginTime = playerLoginTime[identifier]
local currentTime = os.time()
local playtimeSeconds = currentTime - loginTime
local result = CORE:ExecuteSql("SELECT playtime FROM ak4y_multicharacter_v3 WHERE identifier = '" .. identifier .. "'")
local currentPlaytime = 0
if result[1] and result[1].playtime then
currentPlaytime = tonumber(result[1].playtime) or 0
end
local newPlaytime = currentPlaytime + playtimeSeconds
CORE:ExecuteSql("UPDATE ak4y_multicharacter_v3 SET playtime = " .. newPlaytime .. " WHERE identifier = '" .. identifier .. "'")
playerLoginTime[identifier] = nil
end
end
end
end)
RegisterNetEvent('ak4y-multicharacter-v3:server:loadUserData', function(charid, isNew)
local src = source
if Framework then
-- charid should be the full identifier string like "char1:f77e98f..."
-- Same as old script: ak4y-multicharacter-v2/editable/sv_utils.lua:110-113
local identifier = tostring(charid)
-- If identifier doesn't contain ":", it might be database id or invalid
-- Get actual identifier from character data
if not identifier:find(":") then
local license = GetIdentifierForESX(src)
if license then
-- Get character by identifier field matching the charid (which should be identifier)
local charsData = CORE:ExecuteSql("SELECT identifier FROM users WHERE identifier = '" .. identifier .. "'")
if charsData and charsData[1] then
identifier = charsData[1].identifier
else
-- If still not found, try to get by license
local allChars = CORE:ExecuteSql("SELECT identifier FROM users WHERE identifier LIKE 'char%:" .. license .. "' ORDER BY identifier")
if allChars and allChars[1] then
identifier = allChars[1].identifier
else
print("^1[ERROR]^7 Could not find character identifier for: " .. tostring(charid))
return
end
end
else
print("^1[ERROR]^7 Could not get license for player " .. src)
return
end
end
-- Extract "char1" from "char1:f77e98f..." (exactly like old script line 111-112)
local position = identifier:find(":")
local result = nil
if position then
result = identifier:sub(1, position-1)
else
print("^1[ERROR]^7 Invalid identifier format (no colon): " .. identifier)
return
end
if isNew then
awaitingRegistration[src] = result
else
-- Trigger ESX to load player (exactly like old script line 113)
TriggerEvent('esx:onPlayerJoined', src, result)
if Framework.Players then
Framework.Players[GetIdentifierForESX(src)] = true
end
end
end
end)
function GetIdentifierForESX(source)
-- Try license2 first (as per Config.Identifier = "license2")
for _, v in pairs(GetPlayerIdentifiers(source)) do
if string.match(v, "license2:") then
return string.gsub(v, "license2:", "")
end
end
-- Fallback to license
for _, v in pairs(GetPlayerIdentifiers(source)) do
if string.match(v, "license:") then
return string.gsub(v, "license:", "")
end
end
return nil
end
end
local function CreateCitizenId()
local charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
local citizenId = ""
local exists = true
while exists do
citizenId = ""
for i = 1, 8 do
local rand = math.random(1, #charset)
citizenId = citizenId .. string.sub(charset, rand, rand)
end
local result = CORE:ExecuteSql("SELECT * FROM players WHERE citizenid = '" .. citizenId .. "'")
exists = result[1] ~= nil
end
return citizenId
end
if FrameworkName == "qb" then
RegisterNetEvent('ak4y-multicharacter-v3:server:createCharacter', function(data)
local src = source
if Framework then
local citizenid = CreateCitizenId()
local newData = {}
newData.cid = citizenid
newData.charinfo = {
firstname = data.firstname,
lastname = data.lastname,
birthdate = data.birthdate,
gender = data.gender,
nationality = data.nationality or ""
}
if Framework.Player.Login(src, false, newData) then
repeat
Wait(10)
until hasDonePreloading[src]
local randbucket = (GetPlayerPed(src) .. math.random(1,999))
SetPlayerRoutingBucket(src, randbucket)
print('^2[qb-core]^7 '..GetPlayerName(src)..' has succesfully loaded!')
Framework.Commands.Refresh(src)
local varS = {
citizenid = citizenid
}
if Config.UseQbApartments and GetResourceState('qb-apartments') == 'started' then
TriggerClientEvent('apartments:client:setupSpawnUI', src, varS)
else
if GetResourceState('qb-spawn') == 'started' then
TriggerClientEvent('qb-spawn:client:setupSpawns', src, varS, false, nil)
TriggerClientEvent('qb-spawn:client:openUI', src, true)
else
TriggerClientEvent('ak4y-multicharacter-v3:client:closeNUIdefault', src)
end
end
TriggerClientEvent("ak4y-multicharacter-v3:client:closeNUI", src)
end
end
end)
end
if FrameworkName == "esx" then
RegisterNetEvent('ak4y-multicharacter-v3:server:createCharacter', function(data)
local src = source
if Framework then
local xPlayer = Framework.GetPlayerFromId(src)
local identifier = GetIdentifierForESX(src)
if not identifier then
print("^1[ERROR]^7 Could not get identifier for player " .. src)
return
end
local existingChars = CORE:ExecuteSql("SELECT * FROM users WHERE identifier LIKE 'char%:" .. identifier .. "'")
local charNum = #existingChars + 1
awaitingRegistration[src] = charNum
local formattedFirstName = data.firstname
local formattedLastName = data.lastname
local formattedDate = data.birthdate
if xPlayer then
-- Convert data.sex from "m"/"f" to 0/1 if needed
local sexValue = data.sex
if sexValue == "m" then
sexValue = 0
elseif sexValue == "f" then
sexValue = 1
elseif type(sexValue) == "string" and (sexValue == "male" or sexValue == "female") then
sexValue = (sexValue == "male") and 0 or 1
end
playerIdentity[xPlayer.identifier] = {
firstName = formattedFirstName,
lastName = formattedLastName,
dateOfBirth = formattedDate,
sex = sexValue,
height = data.height or 180
}
local currentIdentity = playerIdentity[xPlayer.identifier]
xPlayer.setName(('%s %s'):format(currentIdentity.firstName, currentIdentity.lastName))
xPlayer.set('firstName', currentIdentity.firstName)
xPlayer.set('lastName', currentIdentity.lastName)
xPlayer.set('dateofbirth', currentIdentity.dateOfBirth)
xPlayer.set('sex', currentIdentity.sex)
xPlayer.set('height', currentIdentity.height)
TriggerClientEvent('ak4y-multicharacter:setPlayerData', src, currentIdentity)
-- Save identity to database
CORE:ExecuteSql("UPDATE users SET firstname = '"..currentIdentity.firstName.."', lastname = '"..currentIdentity.lastName.."', dateofbirth = '"..currentIdentity.dateOfBirth.."', sex = '"..currentIdentity.sex.."', height = '"..currentIdentity.height.."' WHERE identifier = '"..xPlayer.identifier.."'")
TriggerEvent('ak4y-multicharacter-v3:completedRegistration', src, data)
else
data.firstname = formattedFirstName
data.lastname = formattedLastName
data.dateofbirth = formattedDate
-- Convert data.sex from "m"/"f" to 0/1 if needed
local sexValue = data.sex
if sexValue == "m" then
sexValue = 0
elseif sexValue == "f" then
sexValue = 1
elseif type(sexValue) == "string" and (sexValue == "male" or sexValue == "female") then
sexValue = (sexValue == "male") and 0 or 1
end
local Identity = {
firstName = formattedFirstName,
lastName = formattedLastName,
dateOfBirth = formattedDate,
sex = sexValue,
height = data.height or 180
}
TriggerEvent('ak4y-multicharacter-v3:completedRegistration', src, data)
TriggerClientEvent('ak4y-multicharacter:setPlayerData', src, Identity)
end
end
end)
AddEventHandler('ak4y-multicharacter-v3:completedRegistration', function(source, data)
TriggerClientEvent("ak4y-multicharacter-v3:created", source)
TriggerEvent('esx:onPlayerJoined', source, "char"..awaitingRegistration[source], data)
awaitingRegistration[source] = nil
if Framework.Players then
Framework.Players[GetIdentifierForESX(source)] = true
end
Wait(1000)
local xPlayer = Framework.GetPlayerFromId(source)
if xPlayer and xPlayer.identifier then
local insertData = {
anim = data.anim or 0,
sound = data.sound or 1,
}
CORE:ExecuteSql("INSERT INTO ak4y_multicharacter_v3 (identifier, xp, anso) VALUES ('" .. xPlayer.identifier .. "', '0', '".. json.encode(insertData) .."') ON DUPLICATE KEY UPDATE anso = '".. json.encode(insertData) .."'")
end
end)
CORE:Register('ak4y-multicharacter-v3:GetAnso', function(source, cid)
if not cid then
local xPlayer = Framework.GetPlayerFromId(source)
if xPlayer then
cid = xPlayer.identifier
end
end
if cid then
local imgData = CORE:ExecuteSql("SELECT * FROM ak4y_multicharacter_v3 WHERE identifier = '" .. cid .. "'")
if imgData[1] then
return imgData[1]
else
return false
end
else
return false
end
end)
end
end)
RegisterNetEvent('ak4y-multicharacter-v3:server:saveAnso', function(identifier, ansoJson)
local src = source
if identifier and ansoJson then
ansoJson = ansoJson:gsub("'", "''")
CORE:ExecuteSql("UPDATE ak4y_multicharacter_v3 SET anso = '" .. ansoJson .. "' WHERE identifier = '" .. identifier .. "'")
end
end)
RegisterNetEvent('ak4y-multicharacter-v3:server:savePhoto', function(identifier, base64Photo)
local src = source
if identifier and base64Photo then
base64Photo = base64Photo:gsub("'", "''")
base64Photo = base64Photo:gsub('"', '\\"')
base64Photo = base64Photo:gsub('\n', '')
base64Photo = base64Photo:gsub('\r', '')
CORE:ExecuteSql("UPDATE ak4y_multicharacter_v3 SET photo = '" .. base64Photo .. "' WHERE identifier = '" .. identifier .. "'")
end
end)
RegisterNetEvent('ak4y-multicharacter-v3:server:saveSelectedPhoto', function(identifier, selectedPhoto)
local src = source
if identifier and selectedPhoto then
selectedPhoto = selectedPhoto:gsub("'", "''")
CORE:ExecuteSql("UPDATE ak4y_multicharacter_v3 SET selectedPhoto = '" .. selectedPhoto .. "' WHERE identifier = '" .. identifier .. "'")
end
end)
Last updated