Skip to main content

Overview

Qbox Core includes a built-in multicharacter system that allows players to create and manage multiple characters per FiveM license. Each character has its own unique identity, inventory, money, and progression.

Character Registration

The character registration data structure defines the basic information needed to create a new character.
types.lua
---@class CharacterRegistration
---@field firstname string
---@field lastname string
---@field nationality string
---@field gender number -- 0 (male) or 1 (female)
---@field birthdate string -- Format: MM-DD-YYYY
---@field cid integer -- Character slot number

Character Slots

The number of character slots available to each player is configurable.

Default Configuration

config.server.lua
config.characters = {
    defaultNumberOfCharacters = 3,
    playersNumberOfCharacters = {
        -- Grant specific licenses more slots
        ['license2:abc123'] = 5,
    }
}

Getting Character Limit

server/character.lua
local function getAllowedAmountOfCharacters(license2, license)
    return config.characters.playersNumberOfCharacters[license2] 
        or license and config.characters.playersNumberOfCharacters[license] 
        or config.characters.defaultNumberOfCharacters
end

Character Creation Flow

1. Fetch Available Characters

When a player connects, the client requests their character list:
server/character.lua
lib.callback.register('qbx_core:server:getCharacters', function(source)
    local license2, license = GetPlayerIdentifierByType(source, 'license2'), 
                              GetPlayerIdentifierByType(source, 'license')
    
    return storage.fetchAllPlayerEntities(license2, license), 
           getAllowedAmountOfCharacters(license2, license)
end)
Returns:
  • Array of existing PlayerEntity objects
  • Maximum number of characters allowed

2. Preview Character Appearance

Before selecting a character, the client can fetch their appearance:
server/character.lua
lib.callback.register('qbx_core:server:getPreviewPedData', function(_, citizenId)
    local ped = storage.fetchPlayerSkin(citizenId)
    if not ped then return end

    return ped.skin, ped.model and joaat(ped.model)
end)

3. Create New Character

When creating a new character:
server/character.lua
lib.callback.register('qbx_core:server:createCharacter', function(source, data)
    local newData = {}
    newData.charinfo = data

    local success = Login(source, nil, newData)
    if not success then return end

    giveStarterItems(source)

    lib.print.info(('%s has created a character'):format(GetPlayerName(source)))
    return newData
end)
The creation process:
  1. Receives character info (name, gender, birthdate, etc.)
  2. Calls Login() with nil citizenid (triggers new character creation)
  3. Gives starter items via ox_inventory
  4. Returns the new character data

4. Load Existing Character

server/character.lua
lib.callback.register('qbx_core:server:loadCharacter', function(source, citizenId)
    local success = Login(source, citizenId)
    if not success then return end

    -- Logging
    logger.log({
        source = 'qbx_core',
        webhook = config.logging.webhook['joinleave'],
        event = 'Loaded',
        color = 'green',
        message = ('**%s** loaded'):format(GetPlayerName(source))
    })
    
    lib.print.info(('%s (Citizen ID: %s ID: %s) has successfully loaded!'):format(
        GetPlayerName(source), citizenId, source
    ))
end)

Starter Items

New characters receive starter items defined in the configuration.
server/character.lua
local starterItems = require 'config.shared'.starterItems

local function giveStarterItems(source)
    if GetResourceState('ox_inventory') == 'missing' then return end
    
    -- Wait for inventory to be ready
    while not exports.ox_inventory:GetInventory(source) do
        Wait(100)
    end
    
    for i = 1, #starterItems do
        local item = starterItems[i]
        if item.metadata and type(item.metadata) == 'function' then
            exports.ox_inventory:AddItem(source, item.name, item.amount, item.metadata(source))
        else
            exports.ox_inventory:AddItem(source, item.name, item.amount, item.metadata)
        end
    end
end

Example Starter Items Configuration

config.shared.lua
config.starterItems = {
    { name = 'phone', amount = 1 },
    { name = 'id_card', amount = 1, metadata = function(source)
        local player = exports.qbx_core:GetPlayer(source)
        return {
            citizenid = player.PlayerData.citizenid,
            firstname = player.PlayerData.charinfo.firstname,
            lastname = player.PlayerData.charinfo.lastname,
            birthdate = player.PlayerData.charinfo.birthdate,
            gender = player.PlayerData.charinfo.gender
        }
    end },
    { name = 'driver_license', amount = 1 },
}

Character Data Initialization

When a character is created or loaded, the CheckPlayerData function initializes all default values.
server/player.lua
function CheckPlayerData(source, playerData)
    playerData = playerData or {}
    
    -- Basic Info
    playerData.citizenid = playerData.citizenid or GenerateUniqueIdentifier('citizenid')
    playerData.cid = playerData.charinfo?.cid or playerData.cid or 1
    playerData.money = playerData.money or {}
    
    -- Initialize money accounts
    for moneytype, startamount in pairs(config.money.moneyTypes) do
        playerData.money[moneytype] = playerData.money[moneytype] or startamount
    end

    -- Charinfo defaults
    playerData.charinfo = playerData.charinfo or {}
    playerData.charinfo.firstname = playerData.charinfo.firstname or 'Firstname'
    playerData.charinfo.lastname = playerData.charinfo.lastname or 'Lastname'
    playerData.charinfo.birthdate = playerData.charinfo.birthdate or '00-00-0000'
    playerData.charinfo.gender = playerData.charinfo.gender or 0
    playerData.charinfo.nationality = playerData.charinfo.nationality or 'USA'
    playerData.charinfo.phone = playerData.charinfo.phone or GenerateUniqueIdentifier('PhoneNumber')
    playerData.charinfo.account = playerData.charinfo.account or GenerateUniqueIdentifier('AccountNumber')
    
    -- Metadata defaults
    playerData.metadata = playerData.metadata or {}
    playerData.metadata.health = playerData.metadata.health or 200
    playerData.metadata.hunger = playerData.metadata.hunger or 100
    playerData.metadata.thirst = playerData.metadata.thirst or 100
    playerData.metadata.stress = playerData.metadata.stress or 0
    playerData.metadata.armor = playerData.metadata.armor or 0
    playerData.metadata.bloodtype = playerData.metadata.bloodtype or config.player.bloodTypes[math.random(1, #config.player.bloodTypes)]
    playerData.metadata.fingerprint = playerData.metadata.fingerprint or GenerateUniqueIdentifier('FingerId')
    playerData.metadata.walletid = playerData.metadata.walletid or GenerateUniqueIdentifier('WalletId')
    
    -- Set job and gang
    local jobs, gangs = storage.fetchPlayerGroups(playerData.citizenid)
    playerData.jobs = jobs or {}
    playerData.gangs = gangs or {}
    
    return CreatePlayer(playerData, Offline)
end

Character Deletion

Delete Character (Player Initiated)

server/character.lua
-- Deprecated event kept for backward compatibility
RegisterNetEvent('qbx_core:server:deleteCharacter', function(citizenId)
    local src = source
    DeleteCharacter(src, citizenId)
    Notify(src, locale('success.character_deleted'), 'success')
end)

Force Delete Character (Admin)

server/commands.lua
lib.addCommand('deletechar', {
    help = locale('info.deletechar_command_help'),
    restricted = 'group.admin',
    params = {
        { name = 'id', help = locale('info.deletechar_command_arg_player_id'), type = 'number' },
    }
}, function(source, args)
    if not IsOptin(source) then 
        Notify(source, locale('error.not_optin'), 'error') 
        return 
    end

    local player = GetPlayer(args.id)
    if not player then return end

    local citizenId = player.PlayerData.citizenid
    ForceDeleteCharacter(citizenId)
    Notify(source, locale('success.character_deleted_citizenid', citizenId))
end)

Character Identifier (CID)

Each character has a cid field representing their slot number (1, 2, 3, etc.).
types.lua
---@field cid integer -- Character ID (slot number)
This is different from citizenid, which is a unique identifier for the character itself.

Storage Integration

Character data is managed through the storage layer:
types.lua
---@class StorageFunctions
---@field fetchPlayerEntity fun(citizenId: string): PlayerEntity?
---@field fetchAllPlayerEntities fun(license2: string, license?: string): PlayerEntity[]
---@field upsertPlayerEntity fun(request: UpsertPlayerRequest)
---@field deletePlayer fun(citizenId: string): boolean

Example: Custom Character Selection UI

-- Client-side callback to get characters
local function getCharacters()
    local characters, maxChars = lib.callback.await('qbx_core:server:getCharacters', false)
    
    print('You have', #characters, 'out of', maxChars, 'characters')
    
    for i, char in ipairs(characters) do
        print(string.format('Character %d: %s %s (CID: %s)', 
            char.cid,
            char.charinfo.firstname,
            char.charinfo.lastname,
            char.citizenid
        ))
    end
    
    return characters, maxChars
end

-- Load selected character
local function loadCharacter(citizenId)
    lib.callback.await('qbx_core:server:loadCharacter', false, citizenId)
end

-- Create new character
local function createCharacter(formData)
    local newChar = lib.callback.await('qbx_core:server:createCharacter', false, {
        firstname = formData.firstname,
        lastname = formData.lastname,
        birthdate = formData.birthdate,
        gender = formData.gender,
        nationality = formData.nationality,
        cid = formData.cid
    })
    
    return newChar
end

Best Practices

Always use citizenid as the unique identifier for characters in your database and scripts. The cid is just the slot number and can be reused.
Use the playersNumberOfCharacters config to grant VIP or donator players additional character slots.
Configure starter items with metadata functions to include character-specific information (e.g., ID cards with names).
Always verify character ownership before allowing deletion. The built-in system handles this automatically.

Build docs developers (and LLMs) love