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